Java是如何使用Curator来操作zookeeper的分布式锁实现抢票功能的

190 阅读3分钟

「这是我参与2022首次更文挑战的第28天,活动详情查看:2022首次更文挑战

软件环境准备

1、springboot+mybatis
2、nginx
3、mysql
4、zookeeper 5、jmeter

实现功能

利用nginx做负载,实现分布式访问的目的,然后进行抢票,抢到票保存到数据库

数据库准备

创建产品表以及订单表。


SET FOREIGN_KEY_CHECKS=0;

-- ----------------------------
-- Table structure for order
-- ----------------------------
DROP TABLE IF EXISTS `order`;
CREATE TABLE `order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `pid` int(11) DEFAULT NULL,
  `user_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1513 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Table structure for product
-- ----------------------------
DROP TABLE IF EXISTS `product`;
CREATE TABLE `product` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `product_name` varchar(255) DEFAULT NULL,
  `stock` int(11) DEFAULT NULL,
  `version` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;

-- ----------------------------
-- Records of product
-- ----------------------------
INSERT INTO `product` VALUES ('1', '动物园门票', '5', '0');

java代码准备

实体类

准备产品类以及订单类

产品类

package com.jony.zookeeper.distributedlock.entity;


public class Product {
    private Integer id;
    private String productName;
    private Integer stock;
    private Integer version;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public Integer getStock() {
        return stock;
    }

    public void setStock(Integer stock) {
        this.stock = stock;
    }

    public Integer getVersion() {
        return version;
    }

    public void setVersion(Integer version) {
        this.version = version;
    }
}

订单类

package com.jony.zookeeper.distributedlock.entity;


public class Order {
  private Integer id;
  private Integer pid;
  private String userId;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public Integer getPid() {
        return pid;
    }

    public void setPid(Integer pid) {
        this.pid = pid;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

dao数据库操作

订单数据操作

将抢购成功的用户订单保存到数据库中

package com.jony.zookeeper.distributedlock.mapper;

import com.jony.zookeeper.distributedlock.entity.Order;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;


@Mapper
public interface OrderMapper {

    @Options(useGeneratedKeys = true,keyColumn = "id",keyProperty = "id")
    @Insert(" insert into `order`(user_id,pid) values(#{userId},#{pid}) ")
    int insert(Order order);
}

产品数据操作

主要查询产品的数量以及库存--操作

package com.jony.zookeeper.distributedlock.mapper;

import com.jony.zookeeper.distributedlock.entity.Product;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;


@Mapper
public interface ProductMapper {

    @Select(" select * from product where id=#{id}  ")
    Product getProduct(@Param("id") Integer id);

    @Update(" update product set stock=stock-1    where id=#{id}  ")
    int deductStock(@Param("id") Integer id);
}

业务逻辑操作

通过调用service方法,首先判断当前库存数量是否大于0,如果大于0则进行库存-1操作,否则抛出异常,另外为了模拟真实场景,在查询数据后,我们将线程暂停500毫秒作为真实处理业务的逻辑。

package com.jony.zookeeper.distributedlock;

import com.jony.zookeeper.distributedlock.entity.Order;
import com.jony.zookeeper.distributedlock.entity.Product;
import com.jony.zookeeper.distributedlock.mapper.OrderMapper;
import com.jony.zookeeper.distributedlock.mapper.ProductMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.UUID;
import java.util.concurrent.TimeUnit;


@Service
public class OrderService {

    @Autowired
    private ProductMapper productMapper;

    @Autowired
    private OrderMapper orderMapper;

    @Transactional
     public void reduceStock(Integer id){
        // 1.    获取库存
        Product product = productMapper.getProduct(id);
        // 模拟耗时业务处理
        sleep( 500); // 其他业务处理

        if (product.getStock() <=0 ) {
            throw new RuntimeException("out of stock");
        }
        // 2.    减库存
        int i = productMapper.deductStock(id);
        if (i==1){
            Order order = new Order();
            order.setUserId(UUID.randomUUID().toString());
            order.setPid(id);
            orderMapper.insert(order);
        }else{
            throw new RuntimeException("deduct stock fail, retry.");
        }

    }

    /**
     * 模拟耗时业务处理
     * @param wait
     */
   public void sleep(long  wait){
       try {
           TimeUnit.MILLISECONDS.sleep( wait );
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
}

Controller访问层

package com.jony.zookeeper.distributedlock;

import org.apache.curator.framework.CuratorFramework;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {


    @Autowired
    private OrderService orderService;

    @Value("${server.port}")
    private String port;

    @RequestMapping("/stock/deduct")
    public Object reduceStock(@RequestParam Integer id) throws Exception {
        try {
            orderService.reduceStock(id);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw e;
            }
        }
        return "ok:" + port;
    }
}

application.properties文件配置

server.port=8080
spring.datasource.url=jdbc:mysql://localhost:3306/jony?serverTimezone=UTC&useSSL=false&useUnicode=true&characterEncoding=UTF-8&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
#该配置项就是指将带有下划线的表字段映射为驼峰格式的实体类属性。
#因此,添加了该项配置后,在开发中只需要根据查询返回的字段,创建好实体类就可以了!
mybatis.configuration.map-underscore-to-camel-case=true

jmeter压测

大家可以自己找一个jmeter下载,我这边就只演示jmeter的压测步骤
1、创建线程组

image.png 2、添加并发条件
10个线程一起并发1次

image.png 2、添加http请求

image.png 3、填写相关参数

image.png 4、添加结果数,用于查看每次请求的情况

image.png 5、执行,并查看结果数

image.png 通过以上执行结果,发现并发10条,全部成功,我们再去查看数据库

查看数据库

1、procuct表中的票数,已经变成了-5 image.png

2、order 表中的数据有10条

image.png 可见,即使在单机服务,在高并发的情况下,普通代码无法保证数据的准确性。

如果是只有单机的情况下,我们可以给Controller代码中加一个sychronized锁,就可以解决问题了,但是在分布式的情况下,只加sychronized是不行的。因此我们可以使用zookeeper的分布式锁来处理。

zookeeper分布式锁的实现

如何使用curator

我们可以打开curator的官网,看下提供的api curator.apache.org/

image.png 查看curator如何帮助我们连接zookeeper

image.png 是不是发现很简单

添加zookeeper的配置信息

package com.jony.zookeeper.distributedlock.zkcfg;

import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CuratorCfg {

    @Bean(initMethod = "start")
    public CuratorFramework curatorFramework(){
        RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3);
        CuratorFramework client = CuratorFrameworkFactory.newClient("192.168.253.131:2181", retryPolicy);
        return client;
    }
}

修改controller方法

1、引入curator,用于连接zookeeper

@Autowired
CuratorFramework curatorFramework;

2、创建节点

InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);

3、加锁

interProcessMutex.acquire();

4、释放锁

interProcessMutex.release();

controller最终

package com.jony.zookeeper.distributedlock;

import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {


    @Autowired
    private OrderService orderService;

    @Value("${server.port}")
    private String port;

    @Autowired
    CuratorFramework curatorFramework;

    @RequestMapping("/stock/deduct")
    public Object reduceStock(@RequestParam Integer id) throws Exception {
        InterProcessMutex interProcessMutex = new InterProcessMutex(curatorFramework, "/product_" + id);
        try {
            interProcessMutex.acquire();
            orderService.reduceStock(id);
        } catch (Exception e) {
            if (e instanceof RuntimeException) {
                throw e;
            }
        }finally {
            interProcessMutex.release();
        }
        return "ok:" + port;
    }

}

以上就可以通过zookeeper实现分布式锁了,由于时间所限,我这边就不再添加nginx做多服务压测了,有兴趣的同学,可以自己试下。