减少库存案例:多线程对共享数据并发操作问题;
线程安全问题:同步代码块和锁机制来解决;
导入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:扣减库存失败!
代码结构如下:
首先我们创建一张锁表,锁表中字段设置唯一约束
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分布式锁的实现基于setnx(set if not exists),设置成功,返回1;设置失败,返回0,释放锁的操作通过del指令来完成
使用redis演示下指令:
创建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演示下指令:
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通过创建临时序列节点来实现分布式锁,适用于顺序执行的程序,大体思路就是创建临时序列节点,找出最小的序列节点,获取分布式锁,程序执行完成之后此序列节点消失,通过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实现分布式队列的步骤: