分布式锁实现方式

分布式锁

分布式锁概述

为什么需要分布式锁

在这里插入图片描述

在单机部署的系统中,使用线程锁来解决并发高的问题,多线程访问共享变量的问题达到数据一致性,如使用synchornized、ReetrantLock等

在这里插入图片描述

但是在后端集群部署的系统中,程序在不同的JVM虚拟机中运行,且因为synchronized或ReentrantLock都只能保证同一个JVM进程中保证有效,所以这是就需要使用分布式锁了

什么是分布式锁

分布式锁其实就是,控制分布式系统不同进程共同访问共享资源的一种锁的实现。如果不同的系统或同一个系统的不同主机间共享了某个临界资源(堆区),往往需要互斥来防止彼此干扰,以保证一致性

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

分布式锁的特点

  1. 互斥性

    任意时刻,只能有一个客户端获取锁,不能有多个获取锁

  2. 高性能

    高性能的获取锁与释放锁

  3. 高可用

    当部分节点down机时,客户端仍能够获取锁和释放锁,具备锁失效机制,防止死锁

  4. 自解锁

    加锁和解锁必须是同一个客户端,客户端不能把别人加的锁给解了

在这里插入图片描述

分布式锁问题_业务介绍

在这里插入图片描述

功能实现

创建表

创建订单表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
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;

-- ----------------------------
-- Records of t_order
-- ----------------------------

SET FOREIGN_KEY_CHECKS = 1;

创建商品表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
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 '商品描述',
    `version` int(255) 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);

SET FOREIGN_KEY_CHECKS = 1;

创建订单商品关联表

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for order_item
-- ----------------------------
DROP TABLE IF EXISTS `order_item`;
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;

-- ----------------------------
-- Records of order_item
-- ----------------------------
INSERT INTO `order_item` VALUES ('1529685823796764675', '1529685823796764674', 1001, 100.00, 1);
INSERT INTO `order_item` VALUES ('1529686091737296898', '1529686091737296897', 1001, 100.00, 1);
INSERT INTO `order_item` VALUES ('1529686091808600067', '1529686091808600066', 1001, 100.00, 1);
INSERT INTO `order_item` VALUES ('1529686091875708931', '1529686091875708930', 1001, 100.00, 1);
INSERT INTO `order_item` VALUES ('1529686092181880835', '1529686092181880834', 1001, 100.00, 1);
INSERT INTO `order_item` VALUES ('1529686092202852354', '1529686092202852353', 1001, 100.00, 1);

SET FOREIGN_KEY_CHECKS = 1;

创建项目

创建SpringBoot项目

在这里插入图片描述

添加相关依赖

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.10</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.chen</groupId>
    <artifactId>FBSTest1</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>FBSTest1</name>
    <description>FBSTest1</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>5.1.48</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.2</version>
        </dependency>
        <!-- 模板引擎 -->
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>2.0</version>
        </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>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>
    
</project>

编写 application.yml

spring:
  redis:
    host: localhost
    port: 6379
  application:
    name: locktest
  datasource:
    url: jdbc:mysql://localhost:3306/129fengbushisuo?serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
server:
  port: 9091
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml

编写主启动类

加入 @MapperScan()注解

package com.chen;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.chen.mapper")
@SpringBootApplication
public class FbsTest1Application {

    public static void main(String[] args) {
        SpringApplication.run(FbsTest1Application.class, args);
    }

}

手动创建或者使用MyBatis-Plus代码生成器生成项目文件

在这里插入图片描述

编写创建订单接口

package com.chen.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.chen.domain.TOrder;

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

在TOrderServiceImpl中实现功能

package com.chen.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chen.domain.OrderItem;
import com.chen.domain.Product;
import com.chen.domain.TOrder;
import com.chen.mapper.OrderItemMapper;
import com.chen.mapper.ProductMapper;
import com.chen.mapper.TOrderMapper;
import com.chen.service.ITOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;

import java.math.BigDecimal;

@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private TOrderMapper orderMapper;


    @Override
    @Transactional
    public String createOrder(Integer productId, Integer count) {
        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if(product == null){
            throw new RuntimeException("购买商品不存在");
        }
        //判断库存是否不足
        if(product.getCount() < count){
            throw new RuntimeException("库存不足");
        }
        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);
        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);
        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);
        return order.getId();
    }
}

编写Controller层

package com.chen.controller;


import com.chen.service.ITOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class TOrderController {

    @Autowired
    private ITOrderService orderService;

    @PostMapping("/create")
    public String createOrder(Integer productId,Integer count){
        return orderService.createOrder(productId, count);
    }
}

使用Postman测试

在这里插入图片描述

使用apache-jmeter测试

设置线程数

在这里插入图片描述

设置HTTP请求参数
在这里插入图片描述

然后启动测试看看我们售卖的数量是否超出了我们的库存数量(库存数量为5)

聚合报告结果

在这里插入图片描述

启动测试之后看看是否只创建了5个订单

可以看到我们在数据库中创建了17个订单,发生了超卖的问题
在这里插入图片描述

下面通过 synchronized锁的方式进行解决

修改我们的TOrderServiceImpl层,加入 @Slf4j 注解

package com.chen.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chen.domain.OrderItem;
import com.chen.domain.Product;
import com.chen.domain.TOrder;
import com.chen.mapper.OrderItemMapper;
import com.chen.mapper.ProductMapper;
import com.chen.mapper.TOrderMapper;
import com.chen.service.ITOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;

import java.math.BigDecimal;

@Slf4j
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private TOrderMapper orderMapper;


    @Override
    @Transactional
    // TODO synchronized 新增代码
    public synchronized String createOrder(Integer productId, Integer count) {
        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if(product == null){
            throw new RuntimeException("购买商品不存在");
        }

        // TODO 新增代码
        log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());

        //判断库存是否不足
        if(product.getCount() < count){
            throw new RuntimeException("库存不足");
        }
        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);
        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);
        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);
        return order.getId();
    }
}

重启项目

把表product的库存数量修改为2看看会不会出现问题,然后使用Jmeter进行测试

在这里插入图片描述
在这里插入图片描述

我们创建了2个库存,但是他帮我们生成了3个订单,具体原因是我们使用 synchronized 是没有问题的他肯定是线程会一个一个的执行相关方法,比如我们第一线程执行完成方法后的最后一个语句后会立即释放我们的锁,这个时候第二线程会立即进入该方法执行操作,这个时候第一个线程的事务提交操作可能还没有完成那么我们进行到该方法的中第二个线程查询的数据还是2,我们访问该方法的线程越多,并发产生的问题就越大

解决方案

当我们锁结束我们的这个线程的操作的事务也应该结束才是最佳方案,所以我们需要手动的提交事务操作才可以

Spring进行了统一的抽象,形成了 PlatformTransactionManager事务管理器接口,事务的提交、回滚等操作全部交给他来实现

事务功能的总体接口设计

三个接口功能一句话总的来说事务管理器基于事务基础信息在操作事务时候对事务状态进行更新

  • PlatformTransactionManager 事务管理器
  • TransactionDefinition 事务的一些基础信息,如超时时间、隔离级别、传播属性等
  • TransactionStatus 事务的一些状态信息,如是否是一个新的事物、是否已被标记为回滚

继续修改我们 TOrderServiceImpl 的 createOrder 方法

package com.chen.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.chen.domain.OrderItem;
import com.chen.domain.Product;
import com.chen.domain.TOrder;
import com.chen.mapper.OrderItemMapper;
import com.chen.mapper.ProductMapper;
import com.chen.mapper.TOrderMapper;
import com.chen.service.ITOrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Transactional;

import java.math.BigDecimal;

@Slf4j
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    @Autowired
    private TOrderMapper orderMapper;

    // TODO
    @Autowired
    private PlatformTransactionManager platformTransactionManager;

    // TODO 事务管理器接口
    @Autowired
    private TransactionDefinition transactionDefinition;

    @Override
    //@Transactional  注释掉这个
    public synchronized String createOrder(Integer productId, Integer count) {

        //TODO 通过事务管理器来创建一个事务的状态对象
        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);

        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if(product == null){
            // TODO 回滚事务
            platformTransactionManager.rollback(transaction);
            throw new RuntimeException("购买商品不存在");
        }

        log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());

        //判断库存是否不足
        if(count > product.getCount()){
            // TODO 回滚事务
            platformTransactionManager.rollback(transaction);
            throw new RuntimeException("库存不足");
        }
        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);
        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);
        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);
        // TODO 提交事务
        platformTransactionManager.commit(transaction);
        return order.getId();
    }
}

再次启动项目使用 Jmeter 进行测试

数据库库存还是设置为2

在这里插入图片描述

此刻是没有错误了,分布式就不一定了

分布式演示

通过IDEA创建两个服务

在这里插入图片描述

启动Nginx服务

在这里插入图片描述

打开nginx的conf目录,然后打开配置文件 nginx.conf 进行配置

修改下图内容

在这里插入图片描述

然后在cmd窗口下使用命令 nginx.exe -s reload 命令重载服务

在这里插入图片描述

更改 Jmeter 端口号

在这里插入图片描述

启动项目 在这里插入图片描述

使用 Jmeter 测试完发现他还是创建了3个订单,说明并发出现了问题

在这里插入图片描述

基于数据库实现分布式锁主要是利用数据库的唯一索引来实现的,唯一索引天然具有排他性,这刚好符合我们对锁的要求,同一时刻只能允许一个竞争者获取锁。我们可以通过两种操作来实现 1.悲观锁 2.乐观锁

使用Redis来实现分布式锁的效率最高,加锁速度最快,因为Redis几乎都是纯内存操作,而基于数据库的方案和基于Zookeeper的方案都会设计到磁盘文件IO,效率相对地下。一般使用Redis来实现分布式锁都是利用Redis的 SETNX key value 这个命令,只有当key不存在时才会执行成功,如果key已经存在则命令执行失败

基于Zookeeper实现的分布式锁

Zookeeper一般用作配置中心,其实现分布式锁的原理和Redsi类似,我们在Zookeeper中创建临时顺序节点,利用节点不能重复创建的特性来保证排他性

悲观锁

悲观锁概述

什么是悲观锁,就是比较悲观的锁,总是假设最坏的情况,每次去拿数据的时候都被认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞知道它拿到锁

通过 for update 进行上锁操作

在MySQL中开启2个命令行界面

在这里插入图片描述

先在第一个窗口执行

START TRANSACTION;  # 开启事务

SELECT * FROM product WHERE id = 1001 FOR UPDATE; # 通过 for update 进行上锁操作

然后在第二个窗口执行一遍发现一直转圈圈

在这里插入图片描述

因为第一个客户端查询数据的使用因为 for update 语句中持有了 1001 这条数据的锁,如果我不提交事务(commit命令),客户端二是不能查询出来数据的,这就是我们所说的悲观锁

那么我们 commit 之后就释放掉锁了

客户端一操作:

在这里插入图片描述

客户端二操作:

在这里插入图片描述

下面从程序中通过悲观锁来解决问题

在 ProductMapper 创建查询方法

package com.chen.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.chen.domain.Product;
import org.apache.ibatis.annotations.Param;

public interface ProductMapper extends BaseMapper<Product> {

    Product findById(@Param("id") Integer id);

}

在 ProductMapper.xml 文件中添加悲观锁查询

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.chen.mapper.ProductMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.chen.domain.Product">
        <id column="id" property="id" />
        <result column="product_name" property="productName" />
        <result column="price" property="price" />
        <result column="count" property="count" />
        <result column="product_desc" property="productDesc" />
        <result column="version" property="version" />
    </resultMap>

    <!-- 悲观锁操作 -->
    <select id="findById" resultType="com.chen.domain.Product">
        select * from product t where t.id = #{id} for update
    </select>

</mapper>

在 ITOrderService中添加悲观锁方法

/**
 * 使用悲观锁实现
 * @param productId
 * @param count
 * @return
*/
String createOrderPessimisticLock(Integer productId,Integer count);

在 TOrderServiceImpl 中实现 createOrderPessimisticLock 方法

/**
 * 使用悲观锁添加订单数据
 * @param productId
 * @param count
 * @return
*/
@Transactional
@Override
public String createOrderPessimisticLock(Integer productId, Integer count) {

    //先查询商品
    Product product = productMapper.selectById(productId);
    //判断对象是否为空
    if (product == null) {
        throw new RuntimeException("购买商品不存在");
    }
    log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());

    //判断库存是否不足
    if (count > product.getCount()) {
        throw new RuntimeException("库存不足");
    }

    //修改商品库存数量
    Integer iCount = product.getCount() - count;
    product.setCount(iCount);
    //更新数据
    productMapper.updateById(product);

    //添加订单数据
    TOrder order = new TOrder();
    order.setOrderStatus(1);
    order.setReceiverName("张三");
    order.setReceiverMobile("15232837159");
    order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
    orderMapper.insert(order);

    //添加订单商品表的数据
    OrderItem orderItem = new OrderItem();
    orderItem.setOrderId(order.getId());
    orderItem.setProduceId(product.getId());
    orderItem.setPurchaseNum(count);
    orderItem.setPurchasePrice(product.getPrice());
    orderItemMapper.insert(orderItem);
    return order.getId();
}

在 TOrderController 中添加 createOrderPessimisticLock 方法

/**
 * 悲观锁操作
 * @param productId
 * @param count
 * @return
*/
@PostMapping("/createPLock")
public String createOrderPessimisticLock(Integer productId,Integer count){
    return orderService.createOrderPessimisticLock(productId, count);
}

再次把数据库中库存修改为2,然后启动项目使用 Jmeter 进行测试

在这里插入图片描述

测试结果

在这里插入图片描述

乐观锁

乐观锁概述

乐观锁用于读多的应用类型,可以提高吞吐量

乐观锁就是假设最好的情况,每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间有没有人去更新这个数据,可以使用版本号机制和 CAS 算法实现

乐观锁实现方式

  • 取出记录时,获取当前version,更新时,当上这个version执行

  • 更新时,set version = newVersion where version = oldVersion,如果version不对,就更新失败

在 ProductMapper 中添加乐观锁更新接口

/**
 * 乐观锁更新
 * @param id
 * @param count
 * @param version
 * @return
*/
int updateProductVersion(@Param("id")Integer id, @Param("count")Integer count, @Param("version") Integer version);

在 ProductMapper.xml 中添加乐观锁更新SQL

<!-- 乐观锁更新 -->
<update id="updateProductVersion">
    UPDATE product t SET t.`count` = t.`count`- #{#count}, t.`version` = t.`version`+ 1
    WHERE t.`id` = ? and t.count > 0 and t.version = #{version}
</update>

在 ITOrderService 中添加乐观锁更新方法

/**
 * 使用乐观锁更新
 * @param productId
 * @param count
 * @return
*/
String createOrderOptimisticLock(Integer productId,Integer count);

在 ITOrderServiceImpl 中实现添加乐观锁更新方法

/**
  * 乐观锁更新操作
  * @param productId
  * @param count
  * @return
*/
@Override
public String createOrderOptimisticLock(Integer productId, Integer count) {
    int retryCount = 0; // 重试次数
    int update = 0; // 更新的结果

    Product product = productMapper.selectById(productId);
    if (product == null) {
        throw new RuntimeException("商品对象为空");
    }
    log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());

    if (count > product.getCount()) {
        throw new RuntimeException("库存不足");
    }

    //这里进行操作
    while (retryCount < 3 && update == 0) {
        update = updateProductRM(productId, count);
        retryCount++;
    }

    if (update == 0) {
        throw new RuntimeException("抢购失败");
    }

    //添加订单
    TOrder order = new TOrder();
    order.setOrderStatus(1);
    order.setReceiverName("王五");
    order.setReceiverMobile("15245857485");
    order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count))); // 总数量
    orderMapper.insert(order);

    //添加订单商品表
    OrderItem orderItem = new OrderItem();
    orderItem.setOrderId(order.getId());
    orderItem.setProduceId(product.getId());
    orderItem.setPurchaseNum(count);
    orderItem.setPurchasePrice(product.getPrice());
    orderItemMapper.insert(orderItem);

    return order.getId();
}

/**
 * 更新商品和版本操作
 * mysql的默认隔离级别为可重复读,导致在同一个事务里面查询3次商品
 * 得到的数据始终是相同的,所以我们提供 updateProductRM,每次操作都会提供一个新的事物,来去做扣减库存操作
 * @param productId
 * @param count
 * @return
*/
private int updateProductRM(Integer productId, int count) {
    int result = 0;
    Product product = productMapper.selectById(productId);
    if (product.getCount() >= count) {
        result = productMapper.updateProductVersion(productId, count, product.getVersion());
    }
    return result;
}

在 TOrderController 中添加 createOrderOptimisticLock 方法

/**
     * 乐观锁更新操作
     * @param productId
     * @param count
     * @return
     */
    @PostMapping("/createOLock")
    public String createOrderOptimisticLock(Integer productId,Integer count){
        return orderService.createOrderOptimisticLock(productId, count);
    }

修改数据库库存数据,然后启动JMeter进行测试 (乐观锁的并发能力强,吞吐量高)

在这里插入图片描述

分布式锁Redis实现的分布式锁原理

获取锁

互斥:确保只有一个线程获得锁

添加锁利用 setnx 的互斥性

启动Redis的我们进行测试看看怎么让某个数据来获得锁

在这里插入图片描述

setnx 命令表示上锁只要是aa被创建出来我们不删除aa或者设定时间自动删除,那么这把锁就不能被是放开

释放锁

手动释放锁

在这里插入图片描述

如果我们的某个服务抢到了锁但是该服务挂机了,这个时候其他服务将不能在抢到锁,形成了死锁,这个时候我们可以通过设置超级时间进行

超时释放锁:获取锁时设置一个超时时间

在这里插入图片描述

但是还是有个问题我们虽然设置了过期时间让他自动过期,但是如果设置的过期时间还没有到,突然又挂机了这个时候我们的锁又称为死锁了,那么我们可以使用超时释放操作

超时释放

两步合成一步

在这里插入图片描述

在这里插入图片描述

添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

在 ITOrderService 层添加 createOrderRedis 方法

/**
 * Redis实现分布式锁
 * @param productId
 * @param count
 * @return
*/
String createOrderRedis(Integer productId,Integer count);

在 TOrderServiceImpl 中实现 createOrderRedis 方法

@Autowired
private StringRedisTemplate redisTemplate;

/**
 * Redis实现分布式锁操作
 * @param productId
 * @param count
 * @return
*/
@Transactional
@Override
public String createOrderRedis(Integer productId, Integer count) {

    String key = "lock";
    Boolean result = redisTemplate.opsForValue().setIfAbsent(key + productId,Thread.currentThread().getId() + "",5, TimeUnit.SECONDS);
    if(!result){
        return "不允许重复下单";
    }

    try {
        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if (product == null) {
            throw new RuntimeException("购买商品不存在");
        }
        log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());
        //判断库存是否不足
        if (count > product.getCount()) {
            throw new RuntimeException("库存不足");
        }

        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);

        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);

        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);
    } catch (RuntimeException e) {
        e.printStackTrace();
    } finally {
        redisTemplate.delete(key + productId);
    }
    return "创建失败";
}

编写 TOrderController 层

/**
 * Redis实现分布式锁
 * @param productId
 * @param count
 * @return
*/
@PostMapping("/createORedis")
public String createOrderRedis(Integer productId,Integer count){
    return orderService.createOrderRedis(productId, count);
}

启动 JMeter 进行测试

在这里插入图片描述

分布式解决方案 Redis 实现的分布式锁原理

在这里插入图片描述

Redis分布式锁误删除问题解决方案

设置超时时间远大于业务执行时间,但是会带来性能问题

删除锁的时候要判断,是不是自己的,如果是再删除

修改 TOrderServiceImpl 层中 createOrderRedis(Integer productId, Integer count) 方法

/**
 * Redis实现分布式锁操作
 * @param productId
 * @param count
 * @return
*/
@Transactional
@Override
public String createOrderRedis(Integer productId, Integer count) {

    String key = "lock";
    Boolean result = redisTemplate.opsForValue().setIfAbsent(key + productId,Thread.currentThread().getId() + "",5, TimeUnit.SECONDS);
    if(!result){
        return "不允许重复下单";
    }

    try {
        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if (product == null) {
            throw new RuntimeException("购买商品不存在");
        }
        log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());
        //判断库存是否不足
        if (count > product.getCount()) {
            throw new RuntimeException("库存不足");
        }

        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);

        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);

        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);

    } catch (RuntimeException e) {
        e.printStackTrace();
    } finally {
        //获得我们线程的ID
        String lockId = redisTemplate.opsForValue().get(key + productId);
        //获取当前线程ID
        String threadId = Thread.currentThread().getId() + "";
        //判断是否一致
        if(lockId.equals(threadId)){
            //如果一致就删除
            redisTemplate.delete(key + productId);
        }
    }
    return "创建失败";
}

重启项目之后再次启动 JMeter 进行测试

在这里插入图片描述

分布式锁解决方案 Redis 分布式锁不可重入问题

Setnx 在极端情况下会出现一下问题

在这里插入图片描述

不可重入:在同一个线程中,无法多次获得同一把锁

不可重试:锁只能获取一次,不能重试

超时释放:虽然可以防止死锁,但是因为业务执行失效特别长也会存在锁的释放有安全问题

主从一致性:主从同步会存在延时特性,造成主机从机数据不一致,或者主机宕机后主机和从机数据不一致

基于Redisson实现分布式锁

Redisson 介绍: https://github.com/redisson/redisson/wiki

Redisson 是一个高级的分布式协调 Redis 客户端,能帮助用户在分布式环境中轻松实现一些 Java 的对象,Redisson、Jedis、Lettuce 是三个不同操作

Redis 的客户端,Jedis、Lettuce 的 API 更侧重对 Redis 数据库的 CRUD,而 Redisson API 侧重于分布式开发

添加依赖

<!--redisson-->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.17.2</version>
</dependency>

编写 Redis 分布式锁工具类,创建 Utils 包、创建 RedissonLockUtils 工具类

package com.chen.utils;

import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Slf4j
@Component
public class RedissonLockUtils {

    @Autowired
    private RedissonClient redissonClient;

    /**
     * 上锁方法
     * @param lockName
     * @return
     */
    public Boolean lock(String lockName){
        //判断客户端是否存在
        if(redissonClient == null){
            log.info("RedissonLock is null");
            return false;
        }

        try {
            //加锁
            RLock lock = redissonClient.getLock(lockName);
            //过期时间为10秒钟
            //这里无需手动解锁,到10秒钟会自动解锁
            lock.lock(10, TimeUnit.SECONDS);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 解锁方法
     * @param lockName
     * @return
     */
    public Boolean unLock(String lockName){
        //判断客户端是否存在
        if(redissonClient == null){
            log.info("RedissonLock is null");
            return false;
        }
        try {
            //获取锁
            RLock lock = redissonClient.getLock(lockName);
            //解锁
            lock.unlock();
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }

}

在 ITOrderService 中添加 createOrderRedisson(Integer productId,Integer count) 方法

/**
 * Redisson 操作
 * @param productId
 * @param count
 * @return
*/
String createOrderRedisson(Integer productId,Integer count);

在 ITOrderServiceImpl 中实现 createOrderRedisson(Integer productId,Integer count) 方法

@Autowired
private RedissonLockUtils redissonLockUtils;

/**
 * Redisson实现分布式锁
 * @param productId
 * @param count
 * @return
*/
@Override
public String createOrderRedisson(Integer productId, Integer count) {

    String key = "lock";
    Boolean lock = redissonLockUtils.lock(key + productId);
    if(!lock){
        return "未获得锁";
    }

    try {
        //先查询商品
        Product product = productMapper.selectById(productId);
        //判断对象是否为空
        if (product == null) {
            throw new RuntimeException("购买商品不存在");
        }
        log.info(Thread.currentThread().getName() + "库存数量不足" + product.getCount());
        //判断库存是否不足
        if (count > product.getCount()) {
            throw new RuntimeException("库存不足");
        }

        //修改商品库存数量
        Integer iCount = product.getCount() - count;
        product.setCount(iCount);
        //更新数据
        productMapper.updateById(product);

        //添加订单数据
        TOrder order = new TOrder();
        order.setOrderStatus(1);
        order.setReceiverName("张三");
        order.setReceiverMobile("15232837159");
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);

        //添加订单商品表的数据
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());
        orderItem.setProduceId(product.getId());
        orderItem.setPurchaseNum(count);
        orderItem.setPurchasePrice(product.getPrice());
        orderItemMapper.insert(orderItem);
        return order.getId();
    } catch (RuntimeException e) {
        e.printStackTrace();
    } finally {
        redissonLockUtils.unLock(key + productId);
    }
    return "创建失败";
}

编写 TOrderController

/**
 * Redisson实现分布式锁
 * @param productId
 * @param count
 * @return
*/
@PostMapping("/createORedisson")
public String createOrderRedisson(Integer productId,Integer count){
    return orderService.createOrderRedisson(productId, count);
}

重启项目使用JMeter进行测试

在这里插入图片描述

Zookeeper分布式锁实现原理

在这里插入图片描述

通过zk实现分布式锁可靠性时最高的

公平锁和可重入锁的原理

取水顺序:

  1. 取水之前,先取号
  2. 号排在前面的,就可以先取水
  3. 先到的排在前面,那些后到的,一个一个挨着在井边排成一堆

公平锁

在这里插入图片描述

这种排队取水模型,就是一种锁的模型

什么是可重入锁呢?

可重入锁

在这里插入图片描述

Zookeeper的节点Znode有四种类型

  • 持久节点:默认的节点类型。创建节点的客户与Zookeeper断开连接后,该节点依旧存在
  • 持久节点顺序节点:所谓顺序节点,在创建节点时,Zookeeper根据创建的时间顺序给该节点名称进行编号,持久节点顺序节点就是有顺序的持久节点
  • 临时节点:和持久节点相反,当创建节点的客户端与Zookeeper断开连接后,临时节点会被删除
  • 临时顺序节点:有顺序的临时节点

创建临时顺序节点:create -e -s /test 123

注意:

  • -e 临时节点
  • -s 顺序节点

创建临时节点

在这里插入图片描述

在这里插入图片描述

ZK分布式锁的实现原理

当第一个客户端请求过来时,Zookeeper客户端会创建一个持久节点 locks ,如果它(Client1)想获得锁,需要在 locks 节点下创建一个顺序节点 lock1

在这里插入图片描述

接着,客户端 Client1 会遍历查找 locks 下面的所有临时顺序子节点,判断自己的子节点 lock1 是不是排序最小的那一个,如果是,则成功获得锁

在这里插入图片描述

这时候如果又来一个客户端 client2 前来尝试获得锁,它会在 locks 下再创建一个临时节点 lock2 看看是否排在最小的那个

在这里插入图片描述

客户端 client2 一样会查找 locks 下面的所有临时顺序子节点,判断自己的节点 lock2 是不是最小的,此时,发现lock1 才是最小的,于是获取锁失败

获取锁失败,它是不会甘心的,client2 向它排序靠前的节点 lock1 注册 Watcher 事件,用来监听 lock1 是否存在,也就是说client2抢锁失败进入等待状态
在这里插入图片描述

此时,如果再来一个客户端Client3来尝试获取锁,它会在 locks 下 再创建一个临时节点 lock3 下再创建一个临时节点lock3
在这里插入图片描述

同样的,client3一样也会查找 locks 下面的所有临时顺序子节点,判断自己的节点 lock3 是不是最小的,发现自己不是最小的,就获取锁失败,它也是不会甘心的,它会向在它前面的节点 lock2 注册 Watcher 注册 Watcher 时间,以监听 lock2 节点是否存在
在这里插入图片描述

释放锁

如果是任务完成 client1 会显示调用删除 lock1 的指令

在这里插入图片描述

如果是客户端故障了,根据临时的特性,lock1 是会自动删除的
在这里插入图片描述

lock1 节点被删除后,client2 因为一直监听着 lock1 ,lock1 节点删除,client2立刻收到通知,也会查找 locks 下面的所有临时顺序子节点,发现 lock2 是最小就获得锁

在这里插入图片描述

同理 client2 获得锁之后,client3 也对它虎视眈眈:

  • Zookeeper 设计定位就是分布式协调,简单易用。如果获取不到锁,只需添加一个监听器即可,很适合做分布式锁
  • Zookeeper 作为分布式锁也缺点:如果有很多的客户端频繁的申请加锁、释放锁,对于 Zookeeper 集群的压力会比较大

基于Zookeeper实现分布式锁

在这里插入图片描述

简介

Apache Curator 是一个比较完善的Zookeeper客户端框架,通过封装的一套高级 API 简化了Zookeeper的操作

Curator主要解决了三类问题

封装Zookeeper client 与 Zookeeper server 之间的连接处理

提供了一套 Fluent 风格的操作 API

提供 Zookeeper 各种应用场景 (比如:分布式锁服务、集群领导选举、共享计数器、缓存机制、分布式队列等)的抽象封装

Curator主要从一下几个方面降低了zk使用的复杂性

重试机制:提供可插拔的重试机制, 它将给捕获所有可恢复的异常配置⼀个重试策略,并且内部也提供了几种标准的重试策略(比如指数补偿)。

连接状态监控:Curator 初始化之后会一直对 zk 连接进行监听,一旦发现连接状态发生变化将会作出相应的处理

zk 客户端实例管理:Curator 会对zk 客户端到 server 集群的连接进行管理,并在需要的时候重建 zk 实例,保证与 zk 集群连接的可靠性

各种使用场景支持:Curator 实现了zk支持的大部分使用场景(甚至包括 zk 自身不支持的场景)这些实现都遵循了 zk 的最佳实践,并考虑了各种极端情况

添加依赖

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-framework</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>5.2.0</version>
</dependency>
<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>5.2.0</version>
</dependency>

创建 config 包 和 ZookeeperConfig 配置类

package com.chen.config;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ZookeeperConfig {

    /**
     * 创建 Curator 的客户端
     * @return
     */
    @Bean
    public CuratorFramework zookeeperClient(){
        CuratorFramework client = CuratorFrameworkFactory.builder()
            .connectString("127.0.0.1:2181") // zk的连接地址
            .sessionTimeoutMs(5000) // 会话的超时时间默认为6秒
            .connectionTimeoutMs(5000) // 连接创建的超时时间
            .retryPolicy(new ExponentialBackoffRetry(1000,3)) //连接的重试次数 (1秒重试3次)
            .build();
        client.start(); //启动
        return client;
    }
}

在 ITOrderService 中添加 createOrderZookeeper 方法

/**
 * zookeeper操作
 * @param productId
 * @param count
 * @return
*/
String createOrderZookeeper(Integer productId,Integer count) throws Exception;

在 TOrderServiceImpl 中实现 createOrderZookeeper 方法

@Autowired
private CuratorFramework curatorFramework;

/**
 * zookeeper操作
 * @param productId
 * @param count
 * @return
 * @throws Exception
*/
@Override
public String createOrderZookeeper(Integer productId, Integer count) throws Exception {
    //创建公平锁
    InterProcessMutex lock = new InterProcessMutex(curatorFramework, "/lockPath");
    //尝试获得锁,我们在这里等待5秒
    //第一个参数为时间,第二个参数为时间的单位
    if(lock.acquire(5,TimeUnit.SECONDS)){
        try {
            //根据商品ID获取商品信息
            Product product = productMapper.selectById(productId);
            if(product == null){
                throw new RuntimeException("购买商品不存在");
            }
            log.info(Thread.currentThread().getName() + "库存数量" + product.getCount());
            //校验库存
            if(count > product.getCount()){
                throw new RuntimeException("库存不足");
            }
            //修改商品库存数量
            Integer iCount = product.getCount() - count;
            product.setCount(iCount);
            //更新数据
            productMapper.updateById(product);

            //添加订单数据
            TOrder order = new TOrder();
            order.setOrderStatus(1);
            order.setReceiverName("张三");
            order.setReceiverMobile("15232837159");
            order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
            orderMapper.insert(order);

            //添加订单商品表的数据
            OrderItem orderItem = new OrderItem();
            orderItem.setOrderId(order.getId());
            orderItem.setProduceId(product.getId());
            orderItem.setPurchaseNum(count);
            orderItem.setPurchasePrice(product.getPrice());
            orderItemMapper.insert(orderItem);
            return order.getId();
        } catch (RuntimeException e) {
            e.printStackTrace();
        } finally {
            //释放锁
            lock.release();
        }
    }
    return "创建失败";
}

在 TOrderController 中添加 createOrderZookeeper方法

/**
 * zookeeper操作
 * @param productId
 * @param count
 * @return
*/
@PostMapping("/createOZookeeper")
public String createOrderZookeeper(Integer productId, Integer count) throws Exception {
    return orderService.createOrderZookeeper(productId, count);
}

启动 redos 、nginx、zookeeper 服务和项目使用 JMeter 进行测试
在这里插入图片描述

三种分布式锁对比

数据库分布式锁实现

优点:简单、使用方便、不需要引入Redis、Zookeeper等中间件

缺点:不适合高并发的场景,db操作性较差

Redis分布式锁实现

优点:性能好,适合高并发场景,较轻量级,有较好的框架支持,如 Redisson

缺点:过期时间不好控制,需要考虑锁被别的线程误删场景

Zookeeper分布式锁实现

优点:有较好的性能和可靠性、有封装的性能和可靠性、有封装较好的框架,如 Curator

缺点:性能不如Redis实现的分布式锁、比较重的分布式锁

汇总对比

从性能角度:Redis > Zookeeper >= 数据库

从实现的复杂性角度:Zookeeper > Redis > 数据库

从可靠性角度:Zookeeper > Redis > 数据库

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值