分布式锁概述
为什么需要分布式锁
在单机部署的系统中,使用线程锁来解决高并发的问题,多线程访问共享变量的问题达到数据一致性,如使用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"?> |
修改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; |
编写创建订单接口
在包com.ss.demo.service下的接口ITOrderService创建订单方法
package com.ss.demo.service; |
在实现类中TorderServiceImpl中进行实现:
package com.ss.demo.service.impl; @Transactional |
修改controller包下的类TOrderController
package com.ss.demo.controller;
|
使用postmain进行测试:
我们使用过Jmeter进行超卖问题的发现
启动测试我们看看是否只创建了5个订单,还是出现超卖的问题
你会发现在数据库中创建了15个订单,发生了超卖的问题
下面我们先通synchronized锁的方式进行解决
修改service我们加锁:
package com.ss.demo.service.impl; |
重新启动项目
把表product的库存数量修改为2我们看看会不会出现问题
我们使用Jmeter进行测试
数据库:
我们明明创建了2个库存,但是他确帮我们生成了3个订单,具体原因是我们使用synchronized是没有问题的他肯定是线程回一个一个的执行相关方法,比如我们第一个线程执行完成方法的最后一个语句后会立即释放我么的锁,这个时候第二个线程会立刻进入该方法中执行操作,这个时候第一个线程的事物提交操作可能还没有完成那么我们进行到该方法的中第二个线程查询的数据还是2,我们访问该方法的线程越多,库存数量越大,并发产生的问题就越大
那么我们该怎么解决呢,具体的想法是我们的锁结束我们的这个线程的操作的事物也应该结束才是最佳方案
所以这个时候我们要选择手动的提交事物操作才可以
Spring进行了统一的抽象,形成了 PlatformTransactionManager事务管理器接口 , 事务的 提交、回滚等操作 全部交给它来实现。
事务功能的总体接口设计
三个接口功能一句话总的来说事务管理器基于事务基础信息在操作 事务时候对事务状态进行更新。
PlatformTransactionManager : 事务管理器
TransactionDefinition : 事务的一些基础信息,如超时时间、隔离级别、传播属性等
TransactionStatus : 事务的一些状态信息,如是否是一个新的事务、是否已被标记为回滚
修改service方法:
package com.ss.demo.service.impl; |
启动项目再次测试
数据库复原为,库存为2
使用Jmeter测试
没毛病,以上单节点是绝对没有问题的,但是分布式就不是了