curator是对zookeeper进行了封装,可以实现分布式锁功能
1.基本原理,zookeeper是dataTree结构,每个节点是一个znode,znode有四种类型,
持久节点(客户端断开连接后znode不会删除),
临时节点(客户端断开连接后znode删除)
持久顺序节点(创建节点时,Zookeeper会根据创建的时间顺序给该节点名称进行编号,持久的)
临时顺序节点(创建节点时,Zookeeper会根据创建的时间顺序给该节点名称进行编号,临时的)
分布式锁就是利用了其中的临时顺序节点来实现的,原理如下:
先创建一个持久znode,连接进来的客户端如果想获得锁就要在这个持久znode下面创建子znode,创建之后客户端会把所有的znode都遍历一遍,看一下自己的znode编号是不是最小的,如果是最小的就获得锁,如果不是最小的,那么就watch他前面的那一个节点避免羊群效应(锁释放后,所有的client都去抢锁),当一个client执行完之后会把自己的znode删除,这时候他后面的znode会收到消息,遍历一遍看自己是不是最小的(既然收到消息,那此时当然是最小的),是最小的,然后就获得锁,以此类推
本节讲一下可重入锁
2.具体代码实现
创建一个springboot工程,导入依赖,和application.xml配置
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.levi</groupId>
<artifactId>zk-code</artifactId>
<version>1.0-SNAPSHOT</version>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-client -->
<!-- <dependency>-->
<!-- <groupId>org.apache.curator</groupId>-->
<!-- <artifactId>curator-client</artifactId>-->
<!-- <version>4.2.0</version>-->
<!-- </dependency>-->
<!-- https://mvnrepository.com/artifact/org.apache.curator/curator-recipes -->
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.2.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>1.7.21</version>
<scope>test</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/log4j/log4j -->
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.17</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.3.2.RELEASE</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-boot-starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mysql连接-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<!-- 自动生成代码-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<!-- velocity模板-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!-- swagger-->
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
<!--swagger-bootstrap-ui-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.2</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
spring:
profiles:
active: dev # 环境设置
application:
name: service-core # 服务名
datasource: # mysql数据库连接
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://host:3306/seckill?serverTimezone=GMT%2B8&characterEncoding=utf-8
username: root
password: root
#mybatis xml文件地址
#mybatis-plus:
# configuration:
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# mapper-locations: classpath:com/levi/mapper/xml/*.xml
zookeeper:
lockpath: /parent-lock/lock
address: host:2181
启动类
package com.levi;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = {"com.levi.mapper"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
数据库建表,简单测试
mybstisplus逆向生成工具
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import org.junit.Test;
/**
* @ClassName CodeGenerator
* @Description TODO
* @Author Levi
* @Date 2021/4/14 10:41
**/
public class CodeGenerator {
@Test
public void genCode() {
// 1、创建代码生成器
AutoGenerator mpg = new AutoGenerator();
// 2、全局配置
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("levi");
gc.setOpen(false); //生成后是否打开资源管理器
gc.setServiceName("%sService"); //去掉Service接口的首字母I
gc.setIdType(IdType.AUTO); //主键策略
gc.setSwagger2(true);//开启Swagger2模式
mpg.setGlobalConfig(gc);
// 3、数据源配置
DataSourceConfig dsc = new DataSourceConfig();
dsc.setUrl("jdbc:mysql://192.168.177.129:3306/seckill?serverTimezone=GMT%2B8&characterEncoding=utf-8");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("St1282001316*");
dsc.setDbType(DbType.MYSQL);
mpg.setDataSource(dsc);
// 4、包配置
PackageConfig pc = new PackageConfig();
pc.setParent("com.levi");
pc.setEntity("pojo"); //此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
mpg.setPackageInfo(pc);
// 5、策略配置
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);//数据库表映射到实体的命名策略
strategy.setColumnNaming(NamingStrategy.underline_to_camel);//数据库表字段映射到实体的命名策略
strategy.setEntityLombokModel(true); // lombok
strategy.setLogicDeleteFieldName("is_deleted");//逻辑删除字段名
strategy.setEntityBooleanColumnRemoveIsPrefix(true);//去掉布尔值的is_前缀(确保tinyint(1))
strategy.setRestControllerStyle(true); //restful api风格控制器
mpg.setStrategy(strategy);
// 6、执行
mpg.execute();
}
}
创建service接口,并且实现里面的方法
package com.levi.service;
public interface SecKillService {
boolean buyProduct(Integer id, Integer quantity) throws Exception;
}
实现类
package com.levi.service.impl;
import com.levi.mapper.ProductMapper;
import com.levi.pojo.Product;
import com.levi.service.SecKillService;
import lombok.extern.slf4j.Slf4j;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.ExponentialBackoffRetry;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
/**
* 秒杀主要逻辑在这个里面
*/
@Service
@Slf4j
public class SecKillServiceImpl implements SecKillService {
@Value("${zookeeper.lockpath}")
private String lockpath;
@Value("${zookeeper.address}")
private String address;
@Resource
private ProductMapper productMapper;
@Override
public boolean buyProduct(Integer id, Integer quantity) throws Exception {
RetryPolicy retry = new ExponentialBackoffRetry(1000, 3);
CuratorFramework zookeeperClient = CuratorFrameworkFactory.newClient(address, 60000, 15000, retry);
zookeeperClient.start();
// create lock
InterProcessMutex lock = new InterProcessMutex(zookeeperClient, lockpath);
//等待2秒看有没有得到锁
if ( lock.acquire(2,TimeUnit.SECONDS) ) {
log.info("{}抢到了锁",Thread.currentThread().getId());
try {
// do some work inside of the critical section here
Product product = productMapper.selectById(1);
if (product.getStack() == 0 || product.getStack()<quantity) {
return false;
}
product.setStack(product.getStack() - 1);
productMapper.updateById(product);
}catch (Exception e){
e.printStackTrace();
} finally {
lock.release();
log.info("{}释放了锁",Thread.currentThread().getId());
}
}else{
//没有得到锁,直接返回false
return false;
}
return true;
}
}
创建controller测试,每个人买一个商品
package com.levi.controller;
import com.levi.service.SecKillService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("zk")
@Slf4j
public class SecKillController {
@Autowired
private SecKillService secKillService;
@GetMapping("secKill")
public void secKill() throws Exception {
if (!secKillService.buyProduct(1, 1)) {
log.info("{}购买时候商品库存不足",Thread.currentThread().getId());
System.out.println();
}else {
log.info("{}购买商品成功",Thread.currentThread().getId());
}
}
}
启动类,启动
package com.levi;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan(basePackages = {"com.levi.mapper"})
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
使用jmeter测试,创建10个线程,每个人买一个,但是数据库只有5个
最后只有5个购买成功
总结:zookeeper的锁力度大,redis的锁快