6 zookeeper實現分散式鎖

word發表於2022-07-17

zookeeper實現分散式鎖

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

  • 鎖:我們在多執行緒中接觸過,作用就是讓當前的資源不會被其他執行緒訪問!
    我的日記本,不可以被別人看到。所以要鎖在保險櫃中
    當我開啟鎖,將日記本拿走了,別人才能使用這個保險櫃
  • 在zookeeper中使用傳統的鎖引發的 “羊群效應” :1000個人建立節點,只有一個人能成功,999
    人需要等待!
  • 羊群是一種很散亂的組織,平時在一起也是盲目地左衝右撞,但一旦有一隻頭羊動起來,其他的羊
    也會不假思索地一哄而上,全然不顧旁邊可能有的狼和不遠處更好的草。羊群效應就是比喻人都有
    一種從眾心理,從眾心理很容易導致盲從,而盲從往往會陷入騙局或遭到失敗。

實現分散式鎖大致流程
img

整體思路
img

  1. 所有請求進來,在/lock下建立 臨時順序節點 ,放心,zookeeper會幫你編號排序

  2. 判斷自己是不是/lock下最小的節點

  3. 是,獲得鎖(建立節點)

  4. 否,對前面小我一級的節點進行監聽

  5. 獲得鎖請求,處理完業務邏輯,釋放鎖(刪除節點),後一個節點得到通知(比你年輕的死了,你
    成為最嫩的了)

  6. 重複步驟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
    

    img

安裝

建立一個資料夾,也就是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的 大致這樣介紹吧
  • img

啟動nginx服務

我這個是在/ 目錄底下執行的 你們可以根據 自己所在的目錄去執行

 /usr/local/nginx/sbin/nginx -c /usr/local/nginx/conf/nginx.conf

訪問nginx

nginx的預設埠是80
img

配置nginx

我們所做的配置大概就是

  • 當有請求去訪問我們伺服器,
  • 然後負載到我們處理請求的伺服器 我這裡是兩臺
  • 為了方便 處理請求的這兩臺伺服器 是在我Windows上

開啟配置檔案

# 開啟配置檔案
vim /usr/local/nginx/conf/nginx.conf
  • 圖中 紅框的位置 是需要新增的內容
  • 配置含義: 我們的nginx監聽的是伺服器的80埠 當有請求訪問時 會負載到 look代理裡面 server是處理請求的兩臺伺服器
  • 檢視本機ip Windows ==>ipconfig linux ==> ip a(ip address)

img

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 -- 使用者編號
)
  • 專案目錄結構
    img

新增依賴

簡單解釋一下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>

mybatis.xml

注意哦 :仔細檢視上面的專案結構 建立相應的資料夾

<?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

注意哦 :仔細檢視上面的專案結構 建立相應的資料夾

<?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

注意哦 :仔細檢視上面的專案結構 建立相應的資料夾

這裡也會出現爆紅,後面會自己消失

<?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

/**
 * @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

/**
 * @author : look-word
 * 2022-07-17 10:12
 **/
@Data
public class Order implements Serializable {
    private String id;
    private Integer pid;
    private Integer userid;
}

持久層

ProductMapper

@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

@Mapper
@Component
public interface OrderMapper {
    // 生成訂單
    @Insert("insert into `order` (id,pid,userid) values (#{id},#{pid},#{userid})")
    int insert(Order order);
}

service

ProductService

/**
 * @author : look-word
 * 2022-07-17 10:28
 **/
public interface ProductService {
    // 扣除庫存
    void reduceStock(Integer id) throws Exception;
}

ProductServiceImpl

/**
 * @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

/**
 * @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
    img

還記得我們在pom.xml配置的tomcat的外掛嗎,我們配置的意思是打包(package)之後會自動執行

  • 在執行打包命令之前,先執行clean命令
    img
  • 執行package命令
    img

測試

  • 瀏覽器訪問
http://localhost:8001/product/reduce/1

img

訪問流程
img

**注意**

在使用jmeter測試的時候 需要啟動兩個服務

  • 在啟動第一個之後 去修改pom裡面的build裡面tomcat外掛的埠 8002
  • 記得要重新整理pom檔案,然後再打包啟動即可

啟動jmeter測試

簡單闡述一下:我們會模擬高併發場景下對這個商品的庫存進行扣減

  • 這也就會導致一個問題,會出現商品超賣(出現負的庫存)
  • 出現的原因: 在同一時間,訪問的請求很多。

下載地址

解壓雙擊jmeter.bat啟動
img

建立執行緒組

img

  • 這裡的執行緒數量根據自己電腦去設定
    img

建立請求

img

我們填寫紅框的內容即可就是訪問的地址

  • 我們還需要檢視請求的結果 建立結果樹 右擊會出現

img

配置好這些之後,點選選單欄綠色啟動標誌

  • 會出現彈窗 第一個點yes 第二個點cancel(取消)

去資料庫檢視

  • 沒有啟動前資料庫的庫存
    img

  • 可以看到 出現了 超賣

img

解決超賣

需要用到 zookeeper叢集,搭建的文章

zookeeper分散式鎖不需要我們手寫去實現,有封裝好的依賴,引入即可

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-recipes</artifactId>
    <version>4.2.0</version> <!-- 網友投票最牛逼版本 -->
</dependency>

在控制層中加入分散式鎖的邏輯程式碼

  • 新增了叢集的ip
/**
 * @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個
  • 說明我們的分散式鎖,已經實現了
  • img

springboot版本後續會退出

相關文章