Distributed lock [distributed lock overview, business introduction, create SpringBoot project] (1) - comprehensive detailed explanation (learning summary --- from entry to deepening)

Table of contents

Overview of distributed locks

Distributed Lock Problem_Business Introduction

Distributed lock problem_Create a SpringBoot project


Overview of distributed locks

 Why do you need distributed locks

 In a single-machine deployment system, thread locks are used to solve high concurrency problems, and multi-threaded access to shared variables achieves data consistency, such as using synchronized, ReentrantLock, etc.

 

 However, in the system deployed by the back-end cluster, the program runs in different JVM virtual machines, and because synchronized or ReentrantLock can only guarantee validity in the same JVM process, it is necessary to use distributed locks at this time.

What is a distributed lock

A distributed lock is actually an implementation of a lock that controls different processes in a distributed system to access shared resources. If different systems or different hosts of the same system share a certain critical resource, mutual exclusion is often required to prevent mutual interference and ensure consistency.

 Features of distributed locks

Distributed Lock Problem_Business Introduction

Case introduction

 Technology selection

 create table

Create order form

CREATE TABLE `t_order`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE
utf8_general_ci NOT NULL,
  `order_status` int(1) NULL DEFAULT NULL
COMMENT '订单状态 1 待支付 2已支付',
  `receiver_name` varchar(255) CHARACTER SET
utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '收货人名字',
  `receiver_mobile` varchar(255) CHARACTER SET
utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '收货人手机',
  `order_amount` decimal(10, 2) NULL DEFAULT
NULL COMMENT '订单价格',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE
= utf8_general_ci ROW_FORMAT = Dynamic;

Create product table

CREATE TABLE `product`  (
  `id` int(11) NOT NULL,
  `product_name` varchar(255) CHARACTER SET
utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '商品名字',
  `price` decimal(10, 2) NULL DEFAULT NULL
COMMENT '商品价格',
  `count` bigint(50) UNSIGNED NULL DEFAULT NULL
COMMENT '库存',
  `product_desc` varchar(255) CHARACTER SET
utf8 COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '商品描述',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE
= utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO `product` VALUES (1001,'拯救者',100.00, 5,'好用实惠', 1);

Create order item association table

CREATE TABLE `order_item`  (
  `id` varchar(255) CHARACTER SET utf8 COLLATE
utf8_general_ci NOT NULL,
  `order_id` varchar(36) CHARACTER SET utf8
COLLATE utf8_general_ci NULL DEFAULT NULL
COMMENT '订单ID',
  `produce_id` int(11) NULL DEFAULT NULL
COMMENT '商品ID',
  `purchase_price` decimal(10, 2) NULL DEFAULT
NULL COMMENT '购买价格',
  `purchase_num` int(11) NULL DEFAULT NULL
COMMENT '购买数量',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE
= utf8_general_ci ROW_FORMAT = Dynamic;

Distributed lock problem_Create a SpringBoot project

 Introduce dependencies

   <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.2</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </dependency>
    </dependencies>

Modify the configuration file

spring:
 application:
   name: lock
 datasource:
   url: jdbc:mysql://192.168.66.100:3306/distribute?serverTimezone=UTC
   username: root
   password01: 123456
   driver-class-name: com.mysql.cj.jdbc.Driver
server:
 port: 9091

Write the main startup class

@Slf4j
@MapperScan("com.tong.lock.mapper")
@SpringBootApplication
public class LockdemoApplication {
    public static void main(String[] args) {
       SpringApplication.run(LockdemoApplication.class, args);
        log.info("************** 分布式锁 **************");
   }
}

code generation

Use Mybaits Plus to generate relevant codes for the order table, product table, and order product association table.

package com.tong.lock.utils;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import java.util.Arrays;
import java.util.List;
public class CodeGenerator {
 public static void main(String[] args) {
      FastAutoGenerator.create("jdbc:mysql://192.168.66.100:3306/distribute", "root", "123456")
               .globalConfig(builder -> {
                    builder.author("itbaizhan")// 设置作者
                           .commentDate("MMdd") // 注释日期格式
                           .outputDir(System.getProperty("user.dir")+ "/src/main/java/") // 指定输出目录
                           .fileOverride(); //覆盖文件
               })
                // 包配置
               .packageConfig(builder -> {
                  builder.parent("com.itbaizhan.lock") // 包名前缀
                           .entity("entity")//实体类包名
                           .mapper("mapper")//mapper接口包名
                           .service("service"); //service包名
               })
               .strategyConfig(builder -> {
                    List<String> strings = Arrays.asList("t_order");
                    // 设置需要生成的表名
                    builder.addInclude(strings)
                            // 开始实体类配置
                           .entityBuilder()
                            // 开启lombok模型
                           .enableLombok()
                            //表名下划线转驼峰
                           .naming(NamingStrategy.underline_to_camel)
                            //列名下划线转驼峰
                           .columnNaming(NamingStrategy.underline_to_camel);
               })
               .execute();
   }
}

Write and create an order interface

public interface ITOrderService extends IService<TOrder> {
    /**
     * 创建订单
     * @return
     */
    String createOrder(Integer productId,Integer count);
}

Implement the create order interface

package com.tong.lock.service.impl;
import com.tong.lock.entity.OrderItem;
import com.tong.lock.entity.Product;
import com.tong.lock.entity.TOrder;
import com.tong.lock.mapper.OrderItemMapper;
import com.tong.lock.mapper.ProductMapper;
import com.tong.lock.mapper.TOrderMapper;
import com.tong.lock.service.ITOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.util.concurrent.locks.ReentrantLock;
/**
* <p>
* 服务实现类
* </p>
*
* @author tong
* @since 05-25
*/
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {
    @Resource
    OrderItemMapper orderItemMapper;
    @Resource
    ProductMapper productMapper;    
    /**
     * 创建订单
     * @return
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public  String createOrder(Integer productId,Integer count) {
  // 1、根据商品id查询商品信息
            Product product = productMapper.selectById(productId);
            // 2、判断商品是否存在
            if (product == null){
                throw new RuntimeException("购买商品不存在:" + productId + "不存在");
           }
            // 3、校验库存
            if( count > product.getCount() ){
                throw   new RuntimeException("商品" + productId + "仅剩" + product.getCount() + "件,无法购买");
           }
            // 4、计算库存
            Integer leftCount = product.getCount() - count;
            // 5、更新库存
            product.setCount(leftCount);
            productMapper.updateById(product);
            // 6、 创建订单
            TOrder order = new TOrder();
            order.setOrderStatus(1);//待处理
            order.setReceiverName("张三");
            order.setReceiverMobile("18587781068");
            order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));//订单价格
            baseMapper.insert(order);
            // 7、 创建订单和商品关系数据
            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getId());
            orderItem.setProduceId(product.getId());
            orderItem.setPurchasePrice(product.getPrice());
            orderItem.setPurchaseNum(count);
            orderItemMapper.insert(orderItem);
            return order.getId();
   }
}

Write and create an order api interface

@RestController
@RequestMapping("/order")
public class OrderController {
    @Autowired
    private ITOrderService iOrderService;
    /**
     * 创建订单
     * @param productId 商品id
     * @param count 商品数量
     * @return
     */
    @PostMapping("/create")
    public String createOrder(Integer productId,Integer count){
      return iOrderService.createOrder(productId,count);
   }
}

test order

review:

 

 Single Thread Execution design pattern

airport security

Single Thread Execution mode means that only one thread can access shared resources at the same time, just like a single-plank bridge, only one person is allowed to pass at a time. In simple terms, Single Thread Execution uses exclusive operations to ensure that only one A thread accesses a shared resource. I believe everyone has the experience of flying. Before entering the boarding gate, you must go through a security check. The security checkpoint is similar to a single-plank bridge. Only one person can pass through it at a time. In addition to checking your boarding pass, the staff also need to connect to the Internet Check the ID card information and whether you are carrying dangerous goods, as shown in the picture below.

 

 Not thread safe

First simulate a non-thread-safe security checkpoint class. Passengers (threads) hold their boarding passes and ID cards respectively and accept the inspection by the staff. The sample code is as follows.

package com.tong.chapter14;
public class FlightSecurity {
    private int count = 0;
    private String boardingPass = "null";// 登机牌
    private String idCard = "null";// 身份证
    public void pass(String boardingPass, String idCard) {
             this.boardingPass = boardingPass;
             this.idCard = idCard;
             this.count++;
             check();
}
    private void check() {
    // 简单的业务,当登机牌和身份证首位不相同时则表示检查不通过
    if (boardingPass.charAt(0) != idCard.charAt(0)) {
          throw new RuntimeException("-----Exception-----" + toString());
   }
}
@Override
public String toString() {
           return "FlightSecurity{" + "count=" + count + ", boardingPass='" + boardingPass + '\'' + ", idCard='" + idCard + '\'' + '}';
    }
}

FlightSecurity is relatively simple. It provides a pass method to pass the passenger's boarding pass and ID card to the pass method. In the pass method, the check method is called to check the passenger. The logic of the check is simple enough, only need to check the boarding pass Whether it is equal to the first digit of the ID card (of course, this is very unreasonable in reality, but we agree to do so in order to make the test simple), let's look at the test shown in the following code.

package com.tong.chapter14;
public class FlightSecurityTest {
       static class Passengers extends Thread {
       // 机场安检类
       private final FlightSecurity flightSecurity;
       // 旅客身份证
       private final String idCard;
       // 旅客登机牌
       private final String boardingPass;
       public Passengers(FlightSecurity flightSecurity, String idCard, String boardingPass) {
       this.flightSecurity = flightSecurity;
       this.idCard = idCard;
       this.boardingPass = boardingPass;
}
@Override
public void run() {
   while (true) {
         // 旅客不断地过安检
         flightSecurity.pass(boardingPass, idCard);
       }
    }
}
public static void main(String[] args) {
      // 定义三个旅客,身份证和登机牌首位均相同
       final FlightSecurity flightsecurity = new FlightSecurity();
       new Passengers(flightsecurity, "Al23456", "AF123456").start();
       new Passengers(flightsecurity, "B123456", "BF123456").start();
       new Passengers(flightsecurity, "C123456", "CF123456").start();
    }
}

It seems that every customer is legitimate, because every customer’s ID card and the first letter of the boarding pass are the same, but an error occurs when running the above program, and the error situation is not the same. After running it many times, it is found Two types of error messages, the program output is as follows:

java.lang.RuntimeException: -----Exception-----FlightSecurity{count=218,boardingPass='AF123456', idCard='B123456'}
java.lang.RuntimeException: -----Exception-----FlightSecurity{count=676,boardingPass='BF123456', 

The check for the same initials cannot pass and the check for different initials cannot pass. Why does this happen? Same initials but can't pass? What's even more strange is that the parameters passed in obviously all have the same initials, so why does the error occur that the initials are not the same.

 problem analysis

Same initials but failed check

1) Thread A calls the pass method, passes it to "A123456" and "AF123456" and assigns a value to idcard successfully. Due to the rotation of the CPU scheduler time slice, the execution right of the CPU belongs to the B thread.

2) Thread B calls the pass method, passing in "B123456" and "BF123456" and assigning idcard successfully, overwriting the idCard assigned by thread A.

3) Thread A regains the execution right of the CPU and assigns boardingPass to AF123456, so the check cannot pass.

4) Before outputting toString, thread B successfully overwrites boardingPass to BF123456.

Why are the initials different 

1) Thread A calls the pass method, passes in "A123456" and "AF123456" and assigns values ​​to the id Card successfully. Due to the rotation of the CPU scheduler time slice, the execution right of the CPU belongs to the B thread.

2) Thread B calls the pass method, passes in "B123456" and "BF123456" and assigns the id Card successfully, overwriting the idCard assigned by thread A.

3) Thread A regains the execution right of the CPU and assigns boardingPass to AF123456, so the check cannot pass.

4) Thread A fails the check and outputs idcard="A123456" and boardingPass="BF123456".

 thread safety

The above problem is ultimately the problem of data synchronization. Although the two parameters passed by the thread to the pass method can guarantee 100% the same initials, when assigning values ​​​​to the properties in FlightSecurity, multiple threads will be interleaved. Combined We know from what we have said before that it is necessary to add synchronization protection to shared resources, and the improved code is as follows.

public synchronized void pass(String boardingPass, String idCard) {
       this.boardingPass = boardingPass;
       this.idCard = idCard;
       this.count++;
       check();
}

After the modified pass method, no matter how long it runs, there will be no more check errors. Why only add the synchronized keyword to the pas method. Both the check and toString methods have access to shared resources. Don’t they? Did it cause an error? Since the check method is executed in the pass method, the pass method plus synchronization has guaranteed single thread execution, so the check method does not need to increase synchronization, and the reason for the toString method is the same.

When is it appropriate to use the single thread execution mode? The answer is as follows.

A. When multiple threads access resources, the synchronized method is always exclusive.

B. When multiple threads change the state of a certain class, such as the boarding pass and ID card of Flightsecurity.

 In Java, you often hear thread-safe classes and thread-unsafe classes. The so-called thread-safe class means that when multiple threads operate on an instance of a certain class at the same time, it will not cause data inconsistency. It is a thread-unsafe class, and the synchronized keyword is often seen in thread-safe classes

 Future design pattern

The Future pattern is somewhat similar to an item order. For example, when shopping online, when you value a certain product, you can submit an order, and when the order is processed, you can just wait at home for the product to be delivered to your door. Or to put it more vividly, when we send an Ajax request, the page is asynchronously processed in the background, and the user does not have to wait for the result of the request, and can continue to browse or operate other content.

Master-Worker design pattern 

The Master-Worker mode is a commonly used parallel computing mode. Its core idea is that the system works collaboratively by two types of processes: Master process and Worker process. Master is responsible for receiving and assigning tasks, and Worker is responsible for processing subtasks. When each Worker-sub-process is processed, the results will be returned to the Master, and the Master will summarize and summarize. The advantage is that a large task can be decomposed into several small tasks and executed in parallel, thereby improving the throughput of the system.

 The specific code implementation logic diagram is as follows:

 producer consumer design pattern

Producers and consumers are also a very classic multithreading model, and we apply a very wide range of ideas in actual development. In the production and consumption mode: there are usually two types of threads, namely several producer threads and several consumer threads. The producer thread is responsible for submitting user requests, and the consumer thread is responsible for specifically processing the tasks submitted by the producer, and communicates between the producer and the consumer through the shared memory buffer.

 Specific code logic implementation ideas:

 Immutable immutable object design pattern

Immutable objects must be thread-safe.

Question about time and date API thread insecurity

Presumably everyone is familiar with SimpleDateFormat. SimpleDateFormat is a very commonly used class in Java for parsing and formatting output of date strings, but it can cause very subtle and difficult to debug problems if not used carefully, because the DateFormat and SimpleDateFormat classes are not thread-safe Yes, calling the format() and parse() methods in a multi-threaded environment should use synchronous code to avoid problems. The thread unsafe issue about the time and date API was not resolved until JDK8 appeared.

 An example of thread-unsafe code is as follows:

package com.tong.chapter18.demo01;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
public class SimpleDateFormatThreadUnsafe {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
      // 初始化时间日期 API
      SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
      // 创建任务线程,执行任务将字符串转成指定格式日期
      Callable<Date> task = () -> sdf.parse("20200808");
      // 创建线程池,数量为 10
      ExecutorService pool = Executors.newFixedThreadPool(10);
      // 构建结果集
      List<Future<Date>> results = new ArrayList<>();
      // 开始执行任务线程,将结果添加至结果集
      for (int i = 0; i < 10; i++) {
             results.add(pool.submit(task));
       }
      // 打印结果集中的内容
      // 在任务线程执行过程中并且访问结果集内容就会报错
      for (Future<Date> future : results) {
             System.out.println(future.get());
     }
      // 关闭线程池
        pool.shutdown();
    }
}

The result of the operation is as follows:

 Let's solve this problem by ourselves first, thread is not safe, is it feasible for me to put it in ThreadLocal?

package com.tong.chapter18.demo01;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 将每次需要格式转换的参数都放入 ThreadLocal 中进行
*/
public class DateFormatThreadLocal {
      private static final ThreadLocal<DateFormat> df = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd"));
      public static Date convert(String source) throws ParseException {
         return df.get().parse(source);
    }
}

Then format the date code as follows:

package com.tong.chapter18.demo01;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.*;
public class SimpleDateFormatThreadSafe {
     public static void main(String[] args) throws ExecutionException,InterruptedException {
        // 初始化时间日期 API
        SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMdd");
        // 创建任务线程,执行任务将字符串转成指定格式日期
        //Callable<Date> task = () -> sdf.parse("20191020");
        // 使用 ThreadLocal 处理非线程安全
        Callable<Date> task = () -> DateFormatThreadLocal.convert("20191020");
        // 创建线程池,数量为 10
         ExecutorService pool = Executors.newFixedThreadPool(10);
        // 构建结果集
         List<Future<Date>> results = new ArrayList<>();
        // 开始执行任务线程,将结果添加至结果集
         for (int i = 0; i < 10; i++) {
               results.add(pool.submit(task));
          }
        // 打印结果集中的内容
        // 在任务线程执行过程中并且访问结果集内容就会报错
         for (Future<Date> future : results) {
               System.out.println(future.get());
          }
         // 关闭线程池
         pool.shutdown();
    }
}

No matter how many times the above program is run, the problem of thread insecurity will not appear again.

Defining Strategies for Immutable Objects

How to define immutable objects? The official documentation describes it as follows:

 Design an immutable object after referring to the official website documentation, as follows:

package com.tong.chapter18.demo02;
public final class Person {
       private final String name;
       private final String address;

       public Person(final String name, final String address) {
            this.name = name;
            this.address = address;
        }

       public String getName() {
             return name;
        }

       public String getAddress() 
            return address;
        }
@Override
public String toString() {
       return "Person{" + "name='" + name + '\'' + ", address='" + address + '\'' + '}';
  }
}

Guess you like

Origin blog.csdn.net/m0_58719994/article/details/131710962