zookeeper实现分布式锁
仓库地址:https://gitee.com/J_look/ssm-zookeeper/blob/master/README.md
- 锁:我们在多线程中接触过,作用就是让当前的资源不会被其他线程访问! 我的日记本,不可以被别人看到。所以要锁在保险柜中 当我打开锁,将日记本拿走了,别人才能使用这个保险柜
- 在zookeeper中使用传统的锁引发的 “羊群效应” :1000个人创建节点,只有一个人能成功,999 人需要等待!
- 羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊 也会不假思索地一哄而上,全然不顾旁边可能有的狼和不远处更好的草。羊群效应就是比喻人都有 一种从众心理,从众心理很容易导致盲从,而盲从往往会陷入骗局或遭到失败。
实现分布式锁大致流程
整体思路
- 所有请求进来,在/lock下创建 临时顺序节点 ,放心,zookeeper会帮你编号排序
- 判断自己是不是/lock下最小的节点
- 是,获得锁(创建节点)
- 否,对前面小我一级的节点进行监听
- 获得锁请求,处理完业务逻辑,释放锁(删除节点),后一个节点得到通知(比你年轻的死了,你 成为最嫩的了)
- 重复步骤2
安装nginx
安装nginx运行所需的库
//一键安装上面四个依赖
yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel
下载nginx
在那个目录下执行这个命令 就会下载到哪个目录下
//下载tar包
wget http://nginx.org/download/nginx-1.13.7.tar.gz
解压
注意哦
解压出来的文件 我们还需要安装哦
下面所有的命令 都是在nginx-1.13.7文件夹里面进行哦
tar -zxvf nginx-1.13.7.tar.gz
查看解压出来的文件
ll ./nginx-1.13.7
安装
创建一个文件夹,也就是nginx需要
安装到的位置
mkdir /usr/local/nginx
执行命令 考虑到后续安装ssl证书 添加两个模块
./configure --with-http_stub_status_module --with-http_ssl_module
执行make install命令
make install
- 我们可以来到nginx安装到的目录下查看
- 你们没有我这么多目录 conf 配置 sbin 启动nginx
博主技术有限
,还没有深入去学习nginx的 大致这样介绍吧
启动nginx服务
我这个是在/ 目录底下执行的 你们可以根据 自己所在的目录去执行
/usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf
访问nginx
nginx的默认端口是80
配置nginx
我们所做的配置大概就是
- 当有请求去访问我们服务器,
- 然后负载到我们处理请求的服务器 我这里是两台
- 为了方便 处理请求的这两台服务器 是在我Windows上
打开配置文件
# 打开配置文件
vim /usr/local/nginx/conf/nginx.conf
- 图中
红框的位置
是需要添加的内容 - 配置含义: 我们的nginx监听的是服务器的80端口 当有请求访问时 会负载到 look代理里面 server是处理请求的两台服务器
查看本机ip
Windows ==>ipconfig linux ==> ip a(ip address)
upstream look{
server 192.168.204.1:8001; //192.168.204.1是我本机的ip地址,8001是tomcat的端口号
server 192.168.204.1:8002; //8002是另外一个工程的tomcat端口号
}
server {
listen 80;
server_name localhost;
#charset koi8-r;
#access_log logs/host.access.log main;
location / {
proxy_pass http://look;
root html;
index index.html index.htm;
}
工程的搭建
搭建ssm框架 有时间推出springboot的版本
- 创建一个maven项目(普通maven项目即可)
创建数据库:
-- 商品表
create table product(
id int primary key auto_increment, -- 商品编号
product_name varchar(20) not null, -- 商品名称
stock int not null, -- 库存
version int not null -- 版本
)
insert into product (product_name,stock,version) values('锦鲤-清空购物车-大奖',5,0)
-- 订单表
create table `order`(
id varchar(100) primary key, -- 订单编号
pid int not null, -- 商品编号
userid int not null -- 用户编号
)
- 项目目录结构
添加依赖
简单解释一下build
- 我们引入的是tomcat7的插件
- configuration 配置的是端口 和根目录
- 注意哦 记得刷新pom文件 build里面会有爆红 不要紧张 不用管他 后面的配置他会自己消失
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>5.2.7.RELEASE</spring.version>
</properties>
<packaging>war</packaging>
<dependencies>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<!-- Mybatis -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.10</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>2.0.7</version>
</dependency>
<!-- 连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.11</version>
</dependency>
<!-- 数据库 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.29</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- maven内嵌的tomcat插件 -->
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<!-- 目前apache只提供了tomcat6和tomcat7两个插件 -->
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<port>8002</port>
<path>/</path>
</configuration>
<executions>
<execution>
<!-- 打包完成后,运行服务 -->
<phase>package</phase>
<goals>
<goal>run</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build></code></pre></div></div><h5 id="108989" name="mybatis.xml">mybatis.xml</h5><blockquote><p> 注意哦 :仔细查看上面的项目结构 创建相应的文件夹
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!-- 后台的日志输出 输出到控制台-->
<settings>
<setting name="logImpl" value="STDOUT_LOGGING"/>
</settings>
</configuration>
spring.xml
注意哦 :仔细查看上面的项目结构 创建相应的文件夹
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- 1.扫描包下的注解 -->
<context:component-scan base-package="controller,service,mapper"/>
<!-- 2.创建数据连接池对象 -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="jdbc:mysql://localhost:3306/2022_zkproduct?serverTimezone=GMT"/>
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="username" value="root"/>
<property name="password" value="317311"/>
<property name="maxActive" value="10"/>
<property name="minIdle" value="5"/>
</bean>
<!-- 3.创建SqlSessionFactory,并引入数据源对象 -->
<bean id="sqlSessionFactory"
class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource"></property>
<property name="configLocation" value="classpath:mybatis/mybatis.xml"></property>
</bean>
<!-- 4.告诉spring容器,数据库语句代码在哪个文件中-->
<!-- mapper.xDao接口对应resources/mapper/xDao.xml-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="mapper"></property>
</bean>
<!-- 5.将数据源关联到事务 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 6.开启事务 -->
<tx:annotation-driven/>
</beans>
web.xml
注意哦 :仔细查看上面的项目结构 创建相应的文件夹
这里也会出现爆红,后面会自己消失
代码语言:javascript复制<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<servlet>
<servlet-name>springMVC</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
<async-supported>true</async-supported>
</servlet>
<servlet-mapping>
<servlet-name>springMVC</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
实体类
- @Data 是lombok的注解
Product
代码语言:javascript复制/**
-
@author : look-word
2022-07-17 10:12
/
@Data
public class Product implements Serializable {
private Integer id;
private String product_name;
private Integer stock;
private Integer version;
}
Order
代码语言:javascript复制/
@author : look-word
2022-07-17 10:12
**/
@Data
public class Order implements Serializable {
private String id;
private Integer pid;
private Integer userid;
}
持久层
ProductMapper
代码语言:javascript复制@Mapper
@Component
public interface ProductMapper {
// 查询商品(目的查库存)
@Select("select * from product where id = #{id}")
Product getProduct(@Param("id") int id);
// 减库存
@Update("update product set stock = stock-1 where id = #{id}")
int reduceStock(@Param("id") int id);
}
OrderMapper
代码语言:javascript复制@Mapper
@Component
public interface OrderMapper {
// 生成订单
@Insert("insert into order
(id,pid,userid) values (#{id},#{pid},#{userid})")
int insert(Order order);
}
service
ProductService
代码语言:javascript复制/**
@author : look-word
2022-07-17 10:28
/
public interface ProductService {
// 扣除库存
void reduceStock(Integer id) throws Exception;
}
ProductServiceImpl
代码语言:javascript复制/
@author : look-word
2022-07-17 10:29
**/
@Transactional
@Service
public class ProductServiceImpl implements ProductService {
@Resource
private ProductMapper productMapper;
@Resource
private OrderMapper orderMapper;
@Override
public void reduceStock(Integer id) throws Exception {
// 查询商品库存
Product product = productMapper.getProduct(id);
if (product.getStock() <= 0) {
throw new RuntimeException("库存不足");
}
// 减库存
int i = productMapper.reduceStock(id);
if (i == 1) {
Order order = new Order();
order.setId(UUID.randomUUID().toString());
order.setUserid(1);
order.setPid(id);
Thread.sleep(500);
orderMapper.insert(order);
} else {
throw new RuntimeException("扣除库存失败");
}
}
}
controller
代码语言:javascript复制/**
@author : look-word
2022-07-17 10:12
**/
@RestController
public class ProductAction {
@Resource
private ProductService productService;
@GetMapping("product/reduce/{id}")
private Object reduce(@PathVariable Integer id) throws Exception {
productService.reduceStock(id);
return "ok";
}
}
启动测试
- 点击右侧的maven
还记得我们在pom.xml配置的tomcat的插件吗,我们配置的意思是打包(package)之后会自动运行
- 在执行打包命令之前,先执行clean命令
- 执行package命令
测试
- 浏览器访问
代码语言:javascript复制http://localhost:8001/product/reduce/1
访问流程
**注意**
在使用jmeter测试的时候 需要启动两个服务
- 在启动第一个之后 去修改pom里面的build里面tomcat插件的端口 8002
- 记得要
刷新pom文件
,然后再打包启动即可
启动jmeter测试
简单阐述一下:我们会模拟高并发场景下对这个商品的库存进行扣减
- 这也就会导致一个问题,会出现商品超卖(出现负的库存)
- 出现的原因: 在同一时间,访问的请求很多。
下载地址
解压双击jmeter.bat启动
创建线程组
- 这里的线程数量根据自己电脑去设置
创建请求
我们填写红框的内容即可就是访问的地址
- 我们还需要查看请求的结果 创建结果树 右击会出现
配置好这些之后,点击菜单栏绿色启动标志
- 会出现弹窗 第一个点yes 第二个点cancel(取消)
去数据库查看
- 没有启动前数据库的库存
- 可以看到 出现了 超卖
解决超卖
需要用到 zookeeper集群,搭建的文章
zookeeper分布式锁不需要我们手写去实现,有封装好的依赖,引入即可
代码语言:javascript复制<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version> <!-- 网友投票最牛逼版本 -->
</dependency>
在控制层中加入分布式锁的逻辑代码
- 添加了集群的ip
代码语言:javascript复制/**
* @author : look-word
* 2022-07-17 10:12
**/
@RestController
public class ProductAction {
@Resource
private ProductService productService;
// 集群ip
private String connectString = "192.168.77.132,192.168.77.131,192.168.77.130";
@GetMapping("product/reduce/{id}")
private Object reduce(@PathVariable Integer id) throws Exception {
// 重试策略 (1000毫秒试1次,最多试3次)
RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
//1.创建curator工具对象
CuratorFramework client = CuratorFrameworkFactory.newClient(connectString, retryPolicy);
client.start();
//2.根据工具对象创建“内部互斥锁”
InterProcessMutex lock = new InterProcessMutex(client, "/product_" + id);
try {
//3.加锁
lock.acquire();
productService.reduceStock(id);
} catch (Exception e) {
if (e instanceof RuntimeException) {
throw e;
}
} finally {
//4.释放锁
lock.release();
}
return "ok";
}
}
启动jmeter去测试,会发现,请求就像排队一样,一个一个出现,数据库也没有超卖现象
- 可以看到 只有前面的5课请求成功了,我们的库存只有5个
- 说明我们的分布式锁,已经实现了
-
springboot版本后续会退出