问题来源
在一个 JVM 里我们要用锁,可以用 synchronized 和 Lock。如果是在多个 JVM 里,该怎么办呢?
解决思路
MySQL 里,对于同一数据库的同一张表,不能插入相同的数据。往数据库里插入数据可以当做上锁的过程。
Redis 里,setnx 可以仅在 key 不存在时插入数据。setnx 操作可以当做上锁的过程。
Zookeeper 也一样,同一 Znode 不能创建两次。创建 Znode 的过程可以当做上锁的过程。
方案比较
模板方法
public interface Lock {
//获取到锁的资源
void getLock();
// 释放锁
void unLock();
}
public abstract class AbstractLock implements Lock{
public void getLock() {
//尝试获取
if (tryLock()) {
//获取成功的操作
System.out.println("##获取lock锁的资源####");
//获取失败
} else {
//等待一段时间
waitLock();
//再次获取
getLock();
}
}
public abstract boolean tryLock();
public abstract void waitLock();
}
MySQL 方案
import javax.annotation.Resource;
public class MysqlLock extends AbstractLock {
@Resource
private LockMapper mapper;
//所有的线程都往数据库插入主键值相同的数据
private static final int LOCK_ID = 1;
//非阻塞式加锁
public boolean tryLock() {
try {
mapper.insert(LOCK_ID);
} catch (Exception e) {
return false;
}
return true;
}
//让当前线程休眠一段时间
public void waitLock() {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unLock() {
mapper.deleteByPrimaryKey(LOCK_ID);
}
}
package com.mysql.mapper;
public interface LockMapper {
//删除数据解锁
int deleteByPrimaryKey(int id);
//新增数据加锁,id为同一个值
int insert(int id);
}
LockMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.mysql.mapper.LockMapper" >
<delete id="deleteByPrimaryKey" parameterType="INTEGER" >
delete from t_lock
where id = #{code,jdbcType=INTEGER}
</delete>
<insert id="insert" parameterType="INTEGER" >
insert into t_lock (id)
values (#{id,jdbcType=INTEGER})
</insert>
</mapper>
Redis 方案
public class RedisLock extends AbstractLock {
private static Jedis getJedis(){
Jedis jedis = new Jedis("192.168.100.14");
jedis.auth("12345678");
return jedis;
}
private static final String KEY = "LOCK_KEY";
private ThreadLocal<String> local = new ThreadLocal();
//阻塞式加锁,使用setNx命令返回OK的加锁成功,并生产随机值
public boolean tryLock() {
//产生随机值,标识本次锁编号
String uuid = UUID.randomUUID().toString();
/**
* key:我们使用key来当锁
* uuid:唯一标识,这个锁是我加的,属于我
* NX:设入模式【SET_IF_NOT_EXIST】--仅当key不存在时,本语句的值才设入
* PX:给key加有效期
* 1000:有效时间为 1 秒
*/
Jedis jedis = getJedis();
String ret = jedis.set(KEY, uuid,"NX","PX",1000);
//设值成功--抢到了锁
if("OK".equals(ret)){
local.set(uuid);//抢锁成功,把锁标识号记录入本线程--- Threadlocal
return true;
}
//key值里面有了,我的uuid未能设入进去,抢锁失败
return false;
}
public void waitLock() {
try {
Thread.currentThread().sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public void unLock() {
//读取lua脚本
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then \n" +
" return redis.call(\"del\",KEYS[1]) \n" +
"else \n" +
" return 0 \n" +
"end";
//通过原始连接连接redis执行lua脚本
Jedis jedis = getJedis();
jedis.eval(script, Arrays.asList(KEY), Arrays.asList(local.get()));
}
}
Zookeeper 方案
public abstract class ZookeeperAbstractLock extends AbstractLock {
// zk连接地址
private static final String CONNECTSTRING = "192.168.100.14:2181,192.168.100.15:2181,192.168.100.16:2181";
// 创建zk连接
protected ZkClient zkClient = new ZkClient(CONNECTSTRING);
protected static final String PATH = "/lock";
protected static final String PATH2 = "/lock2";
}
方案一
public class ZookeeperDistrbuteLock extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch = null;
@Override
//尝试获得锁
public boolean tryLock() {
try {
zkClient.createEphemeral(PATH);
return true;
} catch (Exception e) {
//如果创建失败报出异常
return false;
}
}
@Override
public void waitLock() {
IZkDataListener izkDataListener = new IZkDataListener() {
public void handleDataDeleted(String path) throws Exception {
// 唤醒被等待的线程
if (countDownLatch != null) {
countDownLatch.countDown();
}
}
public void handleDataChange(String path, Object data) throws Exception {
}
};
// 注册事件
zkClient.subscribeDataChanges(PATH, izkDataListener);
//如果节点存
if (zkClient.exists(PATH)) {
countDownLatch = new CountDownLatch(1);
try {
//等待,一直等到接受到事件通知
countDownLatch.await();
} catch (Exception e) {
e.printStackTrace();
}
}
// 删除监听
zkClient.unsubscribeDataChanges(PATH, izkDataListener);
}
public void unLock() {
//释放锁
if (zkClient != null) {
zkClient.delete(PATH);
zkClient.close();
System.out.println("释放锁资源...");
}
}
}
方案二
方案一会出现羊群效应:锁释放资源的时候,会出现多个线程同时竞争锁,造成不必要的消耗。
我们可以改进一下:创建零时顺序节点,按序号来获取锁,这样就不会造成哄抢。
public class ZookeeperDistributeLock2 extends ZookeeperAbstractLock {
private CountDownLatch countDownLatch= null;
private String beforePath;//当前请求的节点前一个节点
private String currentPath;//当前请求的节点
public ZookeeperDistributeLock2() {
if (!this.zkClient.exists(PATH2)) {
this.zkClient.createPersistent(PATH2);
}
}
@Override
public boolean tryLock() {
//如果currentPath为空则为第一次尝试加锁,第一次加锁赋值currentPath
if(currentPath == null || currentPath.length()<= 0){
//创建一个临时顺序节点
currentPath = this.zkClient.createEphemeralSequential(PATH2 + '/',"lock");
}
//获取所有临时节点并排序,临时节点名称为自增长的字符串如:0000000400
List<String> childrens = this.zkClient.getChildren(PATH2);
Collections.sort(childrens);
if (currentPath.equals(PATH2 + '/'+childrens.get(0))) {//如果当前节点在所有节点中排名第一则获取锁成功
return true;
} else {//如果当前节点在所有节点中排名中不是排名第一,则获取前面的节点名称,并赋值给beforePath
int wz = Collections.binarySearch(childrens,
currentPath.substring(7));
beforePath = PATH2 + '/'+childrens.get(wz-1);
}
return false;
}
@Override
public void waitLock() {
IZkDataListener listener = new IZkDataListener() {
public void handleDataDeleted(String dataPath) throws Exception {
if(countDownLatch!=null){
countDownLatch.countDown();
}
}
public void handleDataChange(String dataPath, Object data) throws Exception {
}
};
//给排在前面的的节点增加数据删除的watcher,本质是启动另外一个线程去监听前置节点
this.zkClient.subscribeDataChanges(beforePath, listener);
if(this.zkClient.exists(beforePath)){
countDownLatch=new CountDownLatch(1);
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.zkClient.unsubscribeDataChanges(beforePath, listener);
}
public void unLock() {
//删除当前临时节点
zkClient.delete(currentPath);
zkClient.close();
}
}
相关依赖
<?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>enjoy</groupId>
<artifactId>lock</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.8.1</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.18</version>
</dependency>
<!-- 数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.20</version>
</dependency>
<!-- mybatis相关依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.4.1</version>
</dependency>
<!-- mybatis与spring对接依赖 -->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<!-- spring核心依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<!-- spring jdbc和事务的支持 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.3.3.RELEASE</version>
</dependency>
<!-- 单元测试相关依赖 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.3.2.RELEASE</version>
</dependency>
<!--日志相关依赖-->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.10</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.2</version>
</dependency>
</dependencies>
</project>
参考:deer——Zookeeper