6 zookeeper实现分布式锁

zookeeper实现分布式锁

仓库地址:https://gitee.com/J_look/ssm-zookeeper/blob/master/README.md

  • 锁:我们在多线程中接触过,作用就是让当前的资源不会被其他线程访问! 我的日记本,不可以被别人看到。所以要锁在保险柜中 当我打开锁,将日记本拿走了,别人才能使用这个保险柜
  • 在zookeeper中使用传统的锁引发的 “羊群效应” :1000个人创建节点,只有一个人能成功,999 人需要等待!
  • 羊群是一种很散乱的组织,平时在一起也是盲目地左冲右撞,但一旦有一只头羊动起来,其他的羊 也会不假思索地一哄而上,全然不顾旁边可能有的狼和不远处更好的草。羊群效应就是比喻人都有 一种从众心理,从众心理很容易导致盲从,而盲从往往会陷入骗局或遭到失败。

实现分布式锁大致流程

整体思路

  1. 所有请求进来,在/lock下创建 临时顺序节点 ,放心,zookeeper会帮你编号排序
  2. 判断自己是不是/lock下最小的节点
  3. 是,获得锁(创建节点)
  4. 否,对前面小我一级的节点进行监听
  5. 获得锁请求,处理完业务逻辑,释放锁(删除节点),后一个节点得到通知(比你年轻的死了,你 成为最嫩的了)
  6. 重复步骤2

安装nginx

安装nginx运行所需的库

代码语言:javascript
复制
//一键安装上面四个依赖
yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel

下载nginx

在那个目录下执行这个命令 就会下载到哪个目录下

代码语言:javascript
复制
//下载tar包
wget http://nginx.org/download/nginx-1.13.7.tar.gz

解压

注意哦 解压出来的文件 我们还需要安装哦 下面所有的命令 都是在nginx-1.13.7文件夹里面进行哦

代码语言:javascript
复制
tar -zxvf  nginx-1.13.7.tar.gz

查看解压出来的文件

代码语言:javascript
复制
ll ./nginx-1.13.7

安装

创建一个文件夹,也就是nginx需要安装到的位置

代码语言:javascript
复制
mkdir /usr/local/nginx

执行命令 考虑到后续安装ssl证书 添加两个模块

代码语言:javascript
复制
./configure --with-http_stub_status_module --with-http_ssl_module

执行make install命令

代码语言:javascript
复制
make install
  • 我们可以来到nginx安装到的目录下查看
  • 你们没有我这么多目录 conf 配置 sbin 启动nginx
  • 博主技术有限,还没有深入去学习nginx的 大致这样介绍吧

启动nginx服务

我这个是在/ 目录底下执行的 你们可以根据 自己所在的目录去执行

代码语言:javascript
复制
 /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

访问nginx

nginx的默认端口是80

配置nginx

我们所做的配置大概就是

  • 当有请求去访问我们服务器,
  • 然后负载到我们处理请求的服务器 我这里是两台
  • 为了方便 处理请求的这两台服务器 是在我Windows上

打开配置文件

代码语言:javascript
复制
# 打开配置文件
vim /usr/local/nginx/conf/nginx.conf
  • 图中 红框的位置 是需要添加的内容
  • 配置含义: 我们的nginx监听的是服务器的80端口 当有请求访问时 会负载到 look代理里面 server是处理请求的两台服务器
  • 查看本机ip Windows ==>ipconfig linux ==> ip a(ip address)
代码语言:javascript
复制
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项目即可)
创建数据库:
代码语言:javascript
复制
-- 商品表
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)
代码语言:javascript
复制
-- 订单表
create table `order`(
id varchar(100) primary key, -- 订单编号
pid int not null, -- 商品编号
userid int not null -- 用户编号
)
  • 项目目录结构
添加依赖

简单解释一下build

  • 我们引入的是tomcat7的插件
  • configuration 配置的是端口 和根目录
  • 注意哦 记得刷新pom文件 build里面会有爆红 不要紧张 不用管他 后面的配置他会自己消失
代码语言:javascript
复制
<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>

&lt;dependencies&gt;
    &lt;!-- Spring --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-context&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-beans&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-webmvc&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework&lt;/groupId&gt;
        &lt;artifactId&gt;spring-jdbc&lt;/artifactId&gt;
        &lt;version&gt;${spring.version}&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!-- Mybatis --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mybatis&lt;/groupId&gt;
        &lt;artifactId&gt;mybatis&lt;/artifactId&gt;
        &lt;version&gt;3.5.10&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.mybatis&lt;/groupId&gt;
        &lt;artifactId&gt;mybatis-spring&lt;/artifactId&gt;
        &lt;version&gt;2.0.7&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!-- 连接池 --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;com.alibaba&lt;/groupId&gt;
        &lt;artifactId&gt;druid&lt;/artifactId&gt;
        &lt;version&gt;1.2.11&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!-- 数据库 --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;mysql&lt;/groupId&gt;
        &lt;artifactId&gt;mysql-connector-java&lt;/artifactId&gt;
        &lt;version&gt;8.0.29&lt;/version&gt;
    &lt;/dependency&gt;
    &lt;!-- junit --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;junit&lt;/groupId&gt;
        &lt;artifactId&gt;junit&lt;/artifactId&gt;
        &lt;version&gt;4.13.2&lt;/version&gt;
        &lt;scope&gt;test&lt;/scope&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
        &lt;version&gt;1.18.24&lt;/version&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;

&lt;build&gt;
    &lt;plugins&gt;
        &lt;!-- maven内嵌的tomcat插件 --&gt;
        &lt;plugin&gt;
            &lt;groupId&gt;org.apache.tomcat.maven&lt;/groupId&gt;
            &lt;!-- 目前apache只提供了tomcat6和tomcat7两个插件 --&gt;
            &lt;artifactId&gt;tomcat7-maven-plugin&lt;/artifactId&gt;
            &lt;configuration&gt;
                &lt;port&gt;8002&lt;/port&gt;
                &lt;path&gt;/&lt;/path&gt;
            &lt;/configuration&gt;
            &lt;executions&gt;
                &lt;execution&gt;
                    &lt;!-- 打包完成后,运行服务 --&gt;
                    &lt;phase&gt;package&lt;/phase&gt;
                    &lt;goals&gt;
                        &lt;goal&gt;run&lt;/goal&gt;
                    &lt;/goals&gt;
                &lt;/execution&gt;
            &lt;/executions&gt;
        &lt;/plugin&gt;
    &lt;/plugins&gt;
&lt;/build&gt;</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">
&lt;!-- 1.扫描包下的注解 --&gt;
&lt;context:component-scan base-package=&#34;controller,service,mapper&#34;/&gt;
&lt;!-- 2.创建数据连接池对象 --&gt;
&lt;bean id=&#34;dataSource&#34; class=&#34;com.alibaba.druid.pool.DruidDataSource&#34;
      destroy-method=&#34;close&#34;&gt;
    &lt;property name=&#34;url&#34; value=&#34;jdbc:mysql://localhost:3306/2022_zkproduct?serverTimezone=GMT&#34;/&gt;
    &lt;property name=&#34;driverClassName&#34; value=&#34;com.mysql.jdbc.Driver&#34;/&gt;
    &lt;property name=&#34;username&#34; value=&#34;root&#34;/&gt;
    &lt;property name=&#34;password&#34; value=&#34;317311&#34;/&gt;
    &lt;property name=&#34;maxActive&#34; value=&#34;10&#34;/&gt;
    &lt;property name=&#34;minIdle&#34; value=&#34;5&#34;/&gt;
&lt;/bean&gt;

&lt;!-- 3.创建SqlSessionFactory,并引入数据源对象 --&gt;
&lt;bean id=&#34;sqlSessionFactory&#34;
      class=&#34;org.mybatis.spring.SqlSessionFactoryBean&#34;&gt;
    &lt;property name=&#34;dataSource&#34; ref=&#34;dataSource&#34;&gt;&lt;/property&gt;
    &lt;property name=&#34;configLocation&#34; value=&#34;classpath:mybatis/mybatis.xml&#34;&gt;&lt;/property&gt;
&lt;/bean&gt;

&lt;!-- 4.告诉spring容器,数据库语句代码在哪个文件中--&gt;
&lt;!-- mapper.xDao接口对应resources/mapper/xDao.xml--&gt;
&lt;bean class=&#34;org.mybatis.spring.mapper.MapperScannerConfigurer&#34;&gt;
    &lt;property name=&#34;basePackage&#34; value=&#34;mapper&#34;&gt;&lt;/property&gt;
&lt;/bean&gt;

&lt;!-- 5.将数据源关联到事务 --&gt;
&lt;bean id=&#34;transactionManager&#34;
      class=&#34;org.springframework.jdbc.datasource.DataSourceTransactionManager&#34;&gt;
    &lt;property name=&#34;dataSource&#34; ref=&#34;dataSource&#34;&gt;&lt;/property&gt;
&lt;/bean&gt;

&lt;!-- 6.开启事务 --&gt;
&lt;tx:annotation-driven/&gt;

</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>
&lt;servlet-mapping&gt;
    &lt;servlet-name&gt;springMVC&lt;/servlet-name&gt;
    &lt;url-pattern&gt;/&lt;/url-pattern&gt;
&lt;/servlet-mapping&gt;

</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(&#34;扣除库存失败&#34;);
     }
    

    }
    }

  • 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 = &#34;192.168.77.132,192.168.77.131,192.168.77.130&#34;;
    
    
    @GetMapping(&#34;product/reduce/{id}&#34;)
    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, &#34;/product_&#34; + id);
        try {
            //3.加锁
            lock.acquire();
            productService.reduceStock(id);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw e;
            }
        } finally {
            //4.释放锁
            lock.release();
        }
        return &#34;ok&#34;;
    }
    

    }

    启动jmeter去测试,会发现,请求就像排队一样,一个一个出现,数据库也没有超卖现象

    • 可以看到 只有前面的5课请求成功了,我们的库存只有5个
    • 说明我们的分布式锁,已经实现了

    springboot版本后续会退出