分布式锁-01(单节点解决方案)

分布式锁概述

为什么需要分布式锁

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

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

什么是分布式锁

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

 分布式锁的特点:

 分布式锁问题_业务介绍

 技术选型

 创建表

创建订单表

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项目

加入pom.xml:

<?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.6.13</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.ss.demo</groupId>
    <artifactId>locktest</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>locktest</name>
    <description>Demo project for Spring Boot</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/ssou?serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.jdbc.Driver
server:
  port: 9091
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml

使用mybatis-plus工具反向工程操作对`t_order``product``order_item`三张表进行操作,把生成好的文件拷贝到项目中即可

主启动类:

package com.ss.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@MapperScan("com.ss.demo.mapper")
@SpringBootApplication
public class LocktestApplication {
    public static void main(String[] args) {
        SpringApplication.run(LocktestApplication.class, args);
    }
}

编写创建订单接口

在包com.ss.demo.service下的接口ITOrderService创建订单方法

package com.ss.demo.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.ss.demo.domain.TOrder;

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

在实现类中TorderServiceImpl中进行实现:

package com.ss.demo.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ss.demo.domain.OrderItem;
import com.ss.demo.domain.Product;
import com.ss.demo.domain.TOrder;
import com.ss.demo.mapper.OrderItemMapper;
import com.ss.demo.mapper.ProductMapper;
import com.ss.demo.mapper.TOrderMapper;
import com.ss.demo.service.ITOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
/**
 * <p>
 *  服务实现类
 * </p>
 */
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private TOrderMapper orderMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;
    /**
     * 创建订单方法
     * @param productId     商品Id
     * @param count         购买数量
     * @return
     */

@Transactional
    @Override
    public String createOrder(Integer productId, Integer count) {
        //根据商品id获取商品信息
        Product product = productMapper.selectById(productId);
        if(product == null) {
            throw new RuntimeException("购买商品不存在");
        }
        //校验库存
        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("12345678765");
        //设置订单价格【商品单价*商品数量】
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);  //插入订单操作
        //创建订单商品表的操作
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());     //订单Id
        orderItem.setProduceId(product.getId());  //商品Id
        orderItem.setPurchasePrice(product.getPrice()); //购买价格
        orderItem.setPurchaseNum(count);   //购买数量
        orderItemMapper.insert(orderItem);
        return order.getId();
    }
}

修改controller包下的类TOrderController

package com.ss.demo.controller;
import com.ss.demo.service.ITOrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureOrder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <p>
 *  前端控制器
 * </p>
 */
@RestController
@RequestMapping("/order")
public class TOrderController {
    @Autowired
    private ITOrderService orderService;

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

 

使用postmain进行测试:

我们使用过Jmeter进行超卖问题的发现

 

 

启动测试我们看看是否只创建了5个订单,还是出现超卖的问题

 

你会发现在数据库中创建了15个订单,发生了超卖的问题

下面我们先通synchronized锁的方式进行解决

修改service我们加锁:

package com.ss.demo.service.impl;

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

import java.math.BigDecimal;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Slf4j
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private TOrderMapper orderMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;

    /**
     * 创建订单方法
     * @param productId     商品Id
     * @param count         购买数量
     * @return
     */
    @Transactional
    @Override
    public synchronized String createOrder(Integer productId, Integer count) {
        //根据商品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("12345678765");
        //设置订单价格【商品单价*商品数量】
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);  //插入订单操作
        //创建订单商品表的操作
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());     //订单Id
        orderItem.setProduceId(product.getId());  //商品Id
        orderItem.setPurchasePrice(product.getPrice()); //购买价格
        orderItem.setPurchaseNum(count);   //购买数量
        orderItemMapper.insert(orderItem);
        return order.getId();
    }
}

重新启动项目

把表product的库存数量修改为2我们看看会不会出现问题

我们使用Jmeter进行测试

 

 数据库:

 

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

那么我们该怎么解决呢,具体的想法是我们的锁结束我们的这个线程的操作的事物也应该结束才是最佳方案

所以这个时候我们要选择手动的提交事物操作才可以

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

事务功能的总体接口设计

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

PlatformTransactionManager : 事务管理器

TransactionDefinition : 事务的一些基础信息,如超时时间、隔离级别、传播属性等

TransactionStatus : 事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚

修改service方法:

package com.ss.demo.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ss.demo.domain.OrderItem;
import com.ss.demo.domain.Product;
import com.ss.demo.domain.TOrder;
import com.ss.demo.mapper.OrderItemMapper;
import com.ss.demo.mapper.ProductMapper;
import com.ss.demo.mapper.TOrderMapper;
import com.ss.demo.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;

/**
 * <p>
 *  服务实现类
 * </p>
 */
@Slf4j
@Service
public class TOrderServiceImpl extends ServiceImpl<TOrderMapper, TOrder> implements ITOrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private TOrderMapper orderMapper;

    @Autowired
    private OrderItemMapper orderItemMapper;


    @Autowired
    private PlatformTransactionManager platformTransactionManager;   //事物管理器接口

    @Autowired
    private TransactionDefinition transactionDefinition;

    /**
     * 创建订单方法
     * @param productId     商品Id
     * @param count         购买数量
     * @return
     */
    //@Transactional     //这里要注释掉
    @Override
    public synchronized String createOrder(Integer productId, Integer count) {
        //创建了事物
        TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
        //根据商品id获取商品信息
        Product product = productMapper.selectById(productId);
        if(product == null) {
            platformTransactionManager.rollback(transaction);   //回滚事物
            throw new RuntimeException("购买商品不存在");
        }
        log.info(Thread.currentThread().getName() +"库存数量" + product.getCount());
        //校验库存
        if(count > product.getCount()) {
            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("12345678765");
        //设置订单价格【商品单价*商品数量】
        order.setOrderAmount(product.getPrice().multiply(new BigDecimal(count)));
        orderMapper.insert(order);  //插入订单操作
        //创建订单商品表的操作
        OrderItem orderItem = new OrderItem();
        orderItem.setOrderId(order.getId());     //订单Id
        orderItem.setProduceId(product.getId());  //商品Id
        orderItem.setPurchasePrice(product.getPrice()); //购买价格
        orderItem.setPurchaseNum(count);   //购买数量
        orderItemMapper.insert(orderItem);
        platformTransactionManager.commit(transaction);    //提交事物
        return order.getId();
    }
}

启动项目再次测试

数据库复原为,库存为2

使用Jmeter测试

 

没毛病,以上单节点是绝对没有问题的,但是分布式就不是了

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值