详解zookeeper分布式锁教程

案例:

减少库存案例:多线程对共享数据并发操作问题;
线程安全问题:同步代码块和锁机制来解决;

导入junit依赖


<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.0modelVersion>

    <groupId>com.itheima.demogroupId>
    <artifactId>lock_demoartifactId>
    <version>1.0-SNAPSHOTversion>

    <dependencies>
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
    dependencies>

project>

创建Stock类

public class Stock {
    //库存为1
    private static int num = 1;

    //减少库存数量的方法
    public static boolean reduseStock() {
        if (num > 0) {
            num--;
            return true;
        } else {
            return false;
        }
    }
}

创建StockMain类

public class StockMain {
    static class StockThread implements Runnable {
        public void run() {
            //调用减少库存的方法
            boolean b = new Stock().reduseStock();
            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户1:扣减库存成功!
用户2:扣减库存失败!

如果这时睡眠1秒,看看会出现什么情况:

public class Stock {
    //库存为1
    private static int num = 1;

    //减少库存数量的方法
    public static boolean reduseStock() {
        if (num > 0) {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            num--;
            return true;
        } else {
            return false;
        }
    }
}

结果:

用户2:扣减库存成功!
用户1:扣减库存成功!

这时我们会想到用同步代码块或者锁机制来解决

import com.itheima.demo.bean.Stock;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class StockMain {
    private static Lock lock=new ReentrantLock();
    static class StockThread implements Runnable {
        public void run() {
            //上锁
            lock.lock();
            //调用减少库存的方法
            boolean b = new Stock().reduseStock();
            //解锁
            lock.unlock();
            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户1:扣减库存成功!
用户2:扣减库存失败!

分布式锁

- 数据库实现分布式锁

详解zookeeper分布式锁教程_第1张图片

代码结构如下:

详解zookeeper分布式锁教程_第2张图片

首先我们创建一张锁表,锁表中字段设置唯一约束

CREATE TABLE `lock_record` (
`id` BIGINT ( 20 ) NOT NULL AUTO_INCREMENT COMMENT '主键',
`lock_name` VARCHAR ( 50 ) DEFAULT NULL COMMENT '锁名称',
PRIMARY KEY ( `id` ),
UNIQUE KEY `lock_name` ( `lock_name` ) 
) ENGINE = INNODB AUTO_INCREMENT = 38 DEFAULT CHARSET = utf8

导入依赖:


<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.0modelVersion>

    <groupId>com.itheima.demogroupId>
    <artifactId>lock_demoartifactId>
    <version>1.0-SNAPSHOTversion>


    <properties>
        <spring.version>5.0.2.RELEASEspring.version>
    properties>

    <dependencies>

        <dependency>
            <groupId>redis.clientsgroupId>
            <artifactId>jedisartifactId>
            <version>2.9.0version>
        dependency>

        <dependency>
            <groupId>org.springframework.datagroupId>
            <artifactId>spring-data-redisartifactId>
            <version>2.1.6.RELEASEversion>
        dependency>

        <dependency>
            <groupId>org.apache.commonsgroupId>
            <artifactId>commons-lang3artifactId>
            <version>3.5version>
        dependency>

        <dependency>
            <groupId>org.apache.zookeepergroupId>
            <artifactId>zookeeperartifactId>
            <version>3.4.13version>
        dependency>

        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatisartifactId>
            <version>3.5.1version>
        dependency>

        
        <dependency>
            <groupId>tk.mybatisgroupId>
            <artifactId>mapperartifactId>
            
            <version>4.1.5version>
        dependency>


        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-txartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-aopartifactId>
            <version>${spring.version}version>
        dependency>
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>${spring.version}version>
        dependency>

        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>

        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-testartifactId>
            <version>${spring.version}version>
        dependency>


        <dependency>
            <groupId>org.mybatisgroupId>
            <artifactId>mybatis-springartifactId>
            <version>2.0.1version>
        dependency>

        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.6version>
        dependency>

        <dependency>
            <groupId>com.alibabagroupId>
            <artifactId>druidartifactId>
            <version>1.1.10version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.16.16version>
        dependency>

        
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.6.5version>
        dependency>

        
        <dependency>
            <groupId>org.apache.curatorgroupId>
            <artifactId>curator-recipesartifactId>
            <version>4.0.0version>
        dependency>
    dependencies>

project>

导入配置文件:

applicationContext.xml


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       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">

    
    <context:component-scan base-package="com.Jackie.demo"/>

    
    <context:property-placeholder location="classpath:*.properties">context:property-placeholder>

    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}">property>
        <property name="url" value="${jdbc.url}">property>
        <property name="username" value="${jdbc.username}">property>
        <property name="password" value="${jdbc.password}">property>
    bean>

    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="dataSource">property>
        <property name="typeAliasesPackage" value="com.Jackie.lock.bean">property>
    bean>

    
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="${redis.maxIdle}" />
        <property name="maxWaitMillis" value="${redis.maxWait}" />
        <property name="testOnBorrow" value="${redis.testOnBorrow}" />
    bean>

    <bean id="JedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="${redis.host}">property>
        <property name="port" value="${redis.port}">property>
    bean>


    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="JedisConnectionFactory" />
    bean>


    <bean class="tk.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.Jackie.demo.mapper"/>
    bean>

    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource">property>
    bean>

    <tx:annotation-driven transaction-manager="transactionManager">tx:annotation-driven>

beans>

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/lock?characterEncoding=utf8
jdbc.username=root
jdbc.password=1234

redis.properties

redis.host=127.0.0.1
redis.port=6379
redis.database=0
redis.maxIdle=300
redis.maxWait=3000
redis.testOnBorrow=true

log4j.properties

log4j.rootLogger=DEBUG,stdout
log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
#[%-5p] %t %l %d %rms:%m%n
#%d{yyyy-MM-dd HH:mm:ss,SSS\} %-5p [%t] {%c}-%m%n
log4j.appender.stdout.layout.ConversionPattern=[%-5p] %t %l %d %rms:%m%n
log4j.appender.file=org.apache.log4j.FileAppender
log4j.appender.file.File=E:\\java\\idea\\project\\backend.log
log4j.appender.file.layout=org.apache.log4j.PatternLayout
log4j.appender.file.layout.ConversionPattern=%d{yyyy-MM-dd HH:mm:ss,SSS\\} %-5p [%t] {%c}-%m%n

创建实体类

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Id;
import javax.persistence.Table;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "lock_record")
public class LockRecord {
    @Id
    private Integer id;
    private String lockName;
}

创建Dao层

import com.itheima.demo.bean.LockRecord;
import tk.mybatis.mapper.common.Mapper;

public interface LockRecordMapper extends Mapper<LockRecord> {
}

定义锁,实现Lock接口,tryLock()尝试获取锁,从锁表中查询指定的锁记 录,如果查询到记录,说明已经上锁,不能再上锁

	/**
     * 尝试获取锁:根据指定的名称在数据库表中发起一次查询
     * sql:select * from lock_record where lock_name = "db_lock_stock"
     * @return
     */
    public boolean tryLock() {
        //查询lockRecord的记录
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName",LOCK_NAME);
        LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
        if(lockRecord==null){
            return true;
        }
        return false;
    }

在lock方法获取锁之前先调用tryLock()方法尝试获取锁,如果未加锁则向锁表中插入一条锁记录来获取锁,这里我们通过循环,如果上锁我们一致等待锁的释放

	/**
     * 上锁
     */
    public void lock() {
        while (true) {
            if (tryLock()) {
                //向锁表中插入一条记录
                LockRecord lockRecord = new LockRecord();
                lockRecord.setLockName(LOCK_NAME);
                lockRecordMapper.insertSelective(lockRecord);
                return;
            } else {
                System.out.println("等待锁.......");
            }
        }
    }

释放锁,即是将数据库中对应的锁表记录删除

	/**
     * 释放锁的操作:本质:删除指定名称的纪录
     */
    public void unlock() {
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName",LOCK_NAME);
        lockRecordMapper.deleteByExample(example);
    }

注意在尝试获取锁的方法tryLock中,存在多个线程同时获取锁的情况,可以简单通过synchronized解决

以上完整代码如下:

import com.itheima.demo.bean.LockRecord;
import com.itheima.demo.mapper.LockRecordMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import tk.mybatis.mapper.entity.Example;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Component
public class DbLock implements Lock {

    private static final String LOCK_NAME = "db_lock_stock";

    @Autowired
    private LockRecordMapper lockRecordMapper;

    /**
     * 上锁
     */
    public void lock() {
        while (true) {
            if (tryLock()) {
                //向锁表中插入一条记录
                LockRecord lockRecord = new LockRecord();
                lockRecord.setLockName(LOCK_NAME);
                lockRecordMapper.insertSelective(lockRecord);
                return;
            } else {
                System.out.println("等待锁.......");
            }
        }
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    /**
     * 尝试获取锁:根据指定的名称在数据库表中发起一次查询
     * sql:select * from lock_record where lock_name = "db_lock_stock"
     *
     * @return
     */
    public boolean tryLock() {
        //查询lockRecord的记录
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName", LOCK_NAME);
        LockRecord lockRecord = lockRecordMapper.selectOneByExample(example);
        if (lockRecord == null) {
            return true;
        }
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    /**
     * 释放锁的操作:本质:删除指定名称的纪录
     */
    public void unlock() {
        Example example = new Example(LockRecord.class);
        Example.Criteria criteria = example.createCriteria();
        criteria.andEqualTo("lockName",LOCK_NAME);
        lockRecordMapper.deleteByExample(example);
    }

    public Condition newCondition() {
        return null;
    }
}

测试:

import com.Jackie.demo.bean.Stock;
import com.Jackie.demo.lock.DbLock;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StockMain {
    
    private static DbLock dbLock;

    static{
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        dbLock= context.getBean(DbLock.class);
    }

    static class StockThread implements Runnable {
        public void run() {
            //上锁
            dbLock.lock();

            //调用减少库存的方法
            boolean b = new Stock().reduseStock();

            //解锁
            dbLock.unlock();

            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户1:扣减库存成功!

- redis实现分布式锁

redis分布式锁的实现基于setnx(set if not exists),设置成功,返回1;设置失败,返回0,释放锁的操作通过del指令来完成

使用redis演示下指令:

详解zookeeper分布式锁教程_第3张图片

创建RedisLock类:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Component
public class RedisLock implements Lock {

    private static final String LOCK_NAME = "redis_stock_lock";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 上锁
     */
    public void lock() {
        while (true) {
            Boolean isLock = redisTemplate.opsForValue().setIfAbsent("lockName", LOCK_NAME);
            if (isLock) {
                return;
            } else {
                System.out.println("等待锁........");
            }
        }

    }

    /**
     * 解锁
     */
    public void unlock() {
        // 删除指定的锁的key
        redisTemplate.delete("lockName");
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock() {
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public Condition newCondition() {
        return null;
    }
}

测试:

import com.Jackie.demo.bean.Stock;
import com.Jackie.demo.lock.RedisLock;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StockMain {

    private static RedisLock redisLock;

    static{
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        redisLock = context.getBean(RedisLock.class);
    }

    static class StockThread implements Runnable {
        public void run() {
            //上锁
            redisLock.lock();

            //调用减少库存的方法
            boolean b = new Stock().reduseStock();

            //解锁
            redisLock.unlock();

            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户2:扣减库存成功!
用户1:扣减库存失败!

思考:

//上锁
redisLock.lock();

//调用减少库存的方法
boolean b = new Stock().reduseStock();

//解锁
redisLock.unlock();

如果设置锁后在执行中间过程时,程序抛出异常,导致del指令没有调用,锁永远无法释放,这样就会陷入死锁。所以我们拿到锁之后会给锁加上一个过期时间,这样即使中间出现异常,过期时间到后会自动释放锁。

同时在setnx 和 expire 如果进程挂掉,expire不能执行也会死锁。所以要保证setnx和expire是一个原子性操作即可。redis 2.8之后推出了setnx和expire的组合指令

> set key value ex 5 nx

使用redis演示下指令:

详解zookeeper分布式锁教程_第4张图片

redis实现分布式锁注意的事项:

redis如何避免死锁

代码实现:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

@Component
public class RedisLock implements Lock {

    private static final String LOCK_NAME = "redis_stock_lock";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 上锁
     */
    public void lock() {
        while (true) {
            Boolean isLock =  redisTemplate.opsForValue().setIfAbsent("lockName",LOCK_NAME,15,TimeUnit.SECONDS);
            if (isLock) {
                return;
            } else {
                System.out.println("等待锁........");
            }
        }

    }

    /**
     * 解锁
     */
    public void unlock() {
        // 删除指定的锁的key
        redisTemplate.delete("lockName");
    }

    public void lockInterruptibly() throws InterruptedException {

    }

    public boolean tryLock() {
        return false;
    }

    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    public Condition newCondition() {
        return null;
    }
}

redis实现分布式锁存在的问题,为了解决redis单点问题,我们会部署redis集群,在 Sentinel 集群中,主节点突然挂掉了。同时主节点中有把锁还没有来得及同步到从节点。这样就会导致系统中同样一把锁被两个客户端同时持有,不安全性由此产生。redis官方为了解决这个问题,推出了Redlock 算法解决这个问题。但是带来的网络消耗较大。

分布式锁的redisson实现,导入依赖:

		
        <dependency>
            <groupId>org.redissongroupId>
            <artifactId>redissonartifactId>
            <version>3.6.5version>
        dependency>

获取锁释放锁

import com.Jackie.demo.bean.Stock;
import com.Jackie.demo.lock.RedisLock;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.redisson.config.Config;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StockMain {

    private static RedisLock redisLock;

    private static RLock rLock;

    static{
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        redisLock = context.getBean(RedisLock.class);

        Config config = new Config();
        config.useSingleServer().setAddress("redis://127.0.0.1:6379").setDatabase(0);
        Redisson redisson = (Redisson) Redisson.create(config);
        rLock = redisson.getLock("redis_stock_lock");
    }

    static class StockThread implements Runnable {
        public void run() {
            //上锁
            rLock.lock();

            //调用减少库存的方法
            boolean b = new Stock().reduseStock();

            //解锁
            rLock.unlock();

            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户1:扣减库存成功!
用户2:扣减库存失败!

- zookeeper实现分布式锁

详解zookeeper分布式锁教程_第5张图片

原理:zookeeper通过创建临时序列节点来实现分布式锁,适用于顺序执行的程序,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过watch来监控节点的变化,从剩下的节点的找到最小的序列节点,获取分布式锁,执行相应处理,依次类推……

原生实现

首先在ZkLock的构造方法中,连接zookeeper,创建lock根节点

	//zk客户端
    private ZooKeeper zk;
    //zk是一个目录结构,locks
    private String root = "/locks";
    //锁的名称
    private String lockName;
    //当前线程创建的序列node
    private ThreadLocal<String> nodeId = new ThreadLocal<>();
    //用来同步等待zkclient链接到了服务端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    private final static int sessionTimeout = 3000;
    private final static byte[] data = new byte[0];

	//首先在zkLock的构造方法中,连接zk,创建lock根节点
    public ZkLock(String config, String lockName) {
        this.lockName = lockName;

        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {

                @Override
                public void process(WatchedEvent event) {
                    // 建立连接
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        connectedSignal.countDown();
                    }
                }
            });

            connectedSignal.await();
            Stat stat = zk.exists(root, false);
            if (null == stat) {
                // 创建根节点
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

添加watch监听临时顺序节点的删除

	//添加watch监听临时顺序节点的删除
    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;

        public LockWatcher(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                latch.countDown();
            }
        }
    }

获取锁操作

	@Override
    public void lock() {
        try {
            // 创建临时子节点
            String myNode = zk.create(root + "/" + lockName, data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName() + myNode + "created");

            // 取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for (String node : subNodes) {
                sortedNodes.add(root + "/" + node);
            }

            String smallNode = sortedNodes.first();

            if (myNode.equals(smallNode)) {
                // 如果是最小的节点,则表示取得锁
                System.out.println(Thread.currentThread().getName() + myNode + "get lock");
                this.nodeId.set(myNode);
                return;
            }

            String preNode = sortedNodes.lower(myNode);

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
            // 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
            if (stat != null) {
                System.out.println(Thread.currentThread().getName() + myNode +
                        " waiting for " + root + "/" + preNode + " released lock");

                latch.await();// 等待,这里应该一直等待其他线程释放锁
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

释放锁

	@Override
    public void unlock() {
        try {
            System.out.println(Thread.currentThread().getName() + "unlock ");
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }

完整代码如下:

import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;

import java.util.List;
import java.util.TreeSet;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;


public class ZkLock implements Lock {

    //zk客户端
    private ZooKeeper zk;
    //zk是一个目录结构,locks
    private String root = "/locks";
    //锁的名称
    private String lockName;
    //当前线程创建的序列node
    private ThreadLocal<String> nodeId = new ThreadLocal<>();
    //用来同步等待zkclient链接到了服务端
    private CountDownLatch connectedSignal = new CountDownLatch(1);
    private final static int sessionTimeout = 3000;
    private final static byte[] data = new byte[0];

    public ZkLock(String config, String lockName) {
        this.lockName = lockName;

        try {
            zk = new ZooKeeper(config, sessionTimeout, new Watcher() {

                @Override
                public void process(WatchedEvent event) {
                    // 建立连接
                    if (event.getState() == Event.KeeperState.SyncConnected) {
                        connectedSignal.countDown();
                    }
                }
            });

            connectedSignal.await();
            Stat stat = zk.exists(root, false);
            if (null == stat) {
                // 创建根节点
                zk.create(root, data, ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    //添加watch监听临时顺序节点的删除
    class LockWatcher implements Watcher {
        private CountDownLatch latch = null;

        public LockWatcher(CountDownLatch latch) {
            this.latch = latch;
        }

        @Override
        public void process(WatchedEvent event) {
            if (event.getType() == Event.EventType.NodeDeleted) {
                latch.countDown();
            }
        }
    }

    @Override
    public void lock() {
        try {
            // 创建临时子节点
            String myNode = zk.create(root + "/" + lockName, data, ZooDefs.Ids.OPEN_ACL_UNSAFE,
                    CreateMode.EPHEMERAL_SEQUENTIAL);

            System.out.println(Thread.currentThread().getName() + myNode + "created");

            // 取出所有子节点
            List<String> subNodes = zk.getChildren(root, false);
            TreeSet<String> sortedNodes = new TreeSet<>();
            for (String node : subNodes) {
                sortedNodes.add(root + "/" + node);
            }

            String smallNode = sortedNodes.first();

            if (myNode.equals(smallNode)) {
                // 如果是最小的节点,则表示取得锁
                System.out.println(Thread.currentThread().getName() + myNode + "get lock");
                this.nodeId.set(myNode);
                return;
            }

            String preNode = sortedNodes.lower(myNode);

            CountDownLatch latch = new CountDownLatch(1);
            Stat stat = zk.exists(preNode, new LockWatcher(latch));// 同时注册监听。
            // 判断比自己小一个数的节点是否存在,如果不存在则无需等待锁,同时注册监听
            if (stat != null) {
                System.out.println(Thread.currentThread().getName() + myNode +
                        " waiting for " + root + "/" + preNode + " released lock");

                latch.await();// 等待,这里应该一直等待其他线程释放锁
                nodeId.set(myNode);
                latch = null;
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }


    @Override
    public void unlock() {
        try {
            System.out.println(Thread.currentThread().getName() + "unlock ");
            if (null != nodeId) {
                zk.delete(nodeId.get(), -1);
            }
            nodeId.remove();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (KeeperException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void lockInterruptibly() throws InterruptedException {

    }

    @Override
    public boolean tryLock() {
        return false;
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
        return false;
    }

    @Override
    public Condition newCondition() {
        return null;
    }
}

测试:

import com.Jackie.demo.bean.Stock;
import com.Jackie.demo.lock.RedisLock;
import com.Jackie.demo.lock.ZkLock;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class StockMain {

    private static RedisLock redisLock;
    
    private static ZkLock zkLock;

    static{
        ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
        redisLock = context.getBean(RedisLock.class);

        zkLock = new ZkLock("127.0.0.1:2181","stock_zk");
    }

    static class StockThread implements Runnable {
        public void run() {
            //上锁
            zkLock.lock();

            //调用减少库存的方法
            boolean b = new Stock().reduseStock();

            //解锁
            zkLock.unlock();

            if (b) {
                System.out.println(Thread.currentThread().getName() + ":扣减库存成功!");
            } else {
                System.out.println(Thread.currentThread().getName() + ":扣减库存失败!");
            }
        }
    }

    public static void main(String[] args) {
        new Thread(new StockThread(), "用户1").start();
        new Thread(new StockThread(), "用户2").start();
    }
}

结果:

用户1/locks/stock_zk0000000000created
用户2/locks/stock_zk0000000001created
用户1/locks/stock_zk0000000000get lock
用户2/locks/stock_zk0000000001 waiting for /locks//locks/stock_zk0000000000 released lock
用户1unlock
用户2unlock
用户1:扣减库存成功!
用户2:扣减库存失败!

分布式队列

队列特性:FIFO(先入先出),zookeeper实现分布式队列的步骤:

详解zookeeper分布式锁教程_第6张图片

  • 在队列节点下创建临时顺序节点 例如/queue_info/192.168.1.1-0000001

  • 调用getChildren()接口来获取/queue_info节点下所有子节点,获取队列中所有元素

  • 比较自己节点是否是序号最小的节点,如果不是,则等待其他节点出队列,在序号最小的节点注册watcher

  • 获取watcher通知后,重复步骤详解zookeeper分布式锁教程_第7张图片

你可能感兴趣的:(java,分布式,zookeeper,java,分布式锁,数据库锁)