实现思路 | 优点 | 缺点 | |
---|---|---|---|
利用MySQL数据库实现 | 利用数据库自身提供的锁 要求数据库支持行级锁 |
实现简单,稳定可靠 | 性能差,挖坟适应高并发场景 容易出现死锁的的情况 无法优雅的实现阻塞式锁 |
利用缓存(redis)的实现方案 | 基于redis的setnx命令实现 并通过lua脚本保证解锁时对缓存操作序列的原子性 |
性能好 | 实现相对复杂 又出现死锁的可能性 无法优雅的实现阻塞式锁 |
利用zookeeper的实现方案 | 基于zk节点特性以及watch机制实现 | 性能好,稳定可靠性,能较好的实现阻塞式锁 | 实现相对复杂 |
package com.roger.lock;
import java.util.concurrent.TimeUnit;
public interface DistributeLock {
void lock(String lockKey,String lockValue);
boolean tryLock(String lockKey,String lockValue);
boolean tryLock(String lockKey,String lockValue,long time, TimeUnit timeUnit);
boolean unLock(String lockKey,String lockValue);
}
(1)利用MySQL数据库实现
实现思路:根据数据库表的主键唯一性特点,实现分布式锁,只要当insert成功后,才能获取到锁
package com.roger.lock.impl;
import com.roger.entity.DbDistriLock;
import com.roger.lock.DistributeLock;
import com.roger.mapper.DistributeLockMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service("dbLock")
public class DbLock implements DistributeLock {
@Autowired
private DistributeLockMapper distributeLockMapper;
@Override
public void lock(String lockKey, String lockValue) {
if(!tryLock(lockKey,lockValue)){
//无法优雅的阻塞自己 -- 线程沉睡500ms
waitForLock();
//再次去尝试获取锁,直到获取成功
lock(lockKey,lockValue);
}
}
private void waitForLock() {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
}
}
@Override
public boolean tryLock(String lockKey, String lockValue) {
DbDistriLock dbDistriLock = new DbDistriLock();
dbDistriLock.setId(lockKey);
dbDistriLock.setDistributeLockName(lockValue);
try {
distributeLockMapper.insertDistributeLock(dbDistriLock);
}catch (Exception e){
return false;
}
return true;
}
@Override
public boolean tryLock(String lockKey, String lockValue, long time, TimeUnit timeUnit) {
if(tryLock(lockKey,lockValue)){
return true;
}
if(time <= 0){
return false;
}
long nanoTimes = timeUnit.toNanos(time);
long deadlineTime = System.nanoTime() + nanoTimes;
for (;;){
if(tryLock(lockKey,lockValue)){
return true;
}
nanoTimes = deadlineTime - System.nanoTime();
if(nanoTimes <= 0){
return false;
}
}
}
/**
* 解铃还须系铃人,避免高并发情况下,误解锁的情况
* @param lockKey
* @param lockValue
* @return
*/
@Override
public boolean unLock(String lockKey, String lockValue) {
DbDistriLock dbDistriLock = new DbDistriLock();
dbDistriLock.setId(lockKey);
dbDistriLock.setDistributeLockName(lockValue);
int delNum = distributeLockMapper.delDistriLockByObject(dbDistriLock);
if(delNum == 1){
//解锁成功
return true;
}
return false;
}
}
(2)利用redis实现
实现思路:redis中set命令,NX模式,如果已经存在则不能插入成功,只有当不存在的情况下,才能插入成功
package com.roger.lock.impl;
import com.roger.lock.DistributeLock;
import com.roger.utils.RedisUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import redis.clients.jedis.Jedis;
import java.util.Collections;
import java.util.concurrent.TimeUnit;
@Service("redisLock")
public class RedisLock implements DistributeLock{
//key是唯一的
private static final String LOCK_KEY = "lockKey";
//有key是唯一的前提,配上这个参数,保证在同一时间,只能有一个客户端持有锁
private static final String SET_IF_NOT_EXIST = "NX";
//加锁的值不能是固定值,因为要保证可靠性
//哪个客户端加的锁,需要哪个客户端解锁
//自己不能把别人的锁给解了
private static final String LOCK_VALUE = "lockValue";
private static final String SET_WITH_EXPIRE_TIME = "PX";
private static final String LOCK_SUCCESS = "OK";
private static final Long RELEASE_SUCCESS = 1L;
//保证不会发生死锁的情况,即使有一个客户端在持有锁期间崩溃,
//但是没有主动解锁,到了过期时间,锁会自动解开
private static final long EXPIRE_TIME = 180 * 1000;
@Override
public void lock(String lockKey,String reqestId) {
if(!tryLock(lockKey,reqestId)) {
//无法优雅的阻塞自己 -- 线程沉睡500ms
waitForLock();
//再次尝试获取锁
lock(lockKey, reqestId);
}
}
private void waitForLock() {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
}
}
@Override
public boolean tryLock(String lockKey,String reqestId) {
//多线程环境,不能使用单例模式的jedis实例
Jedis jedis = new Jedis("127.0.0.1", 6379);
String retResult = jedis.set(lockKey, reqestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, EXPIRE_TIME);
if (LOCK_SUCCESS.equals(retResult)) {
return true;
}
return false;
}
@Override
public boolean tryLock(String lockKey,String requestId, long time, TimeUnit timeUnit) {
if (tryLock(lockKey,requestId)) {
return true;
}
if (time < 0) {
return false;
}
long nanoTimeout = timeUnit.toNanos(time);
long deadLine = System.nanoTime() + nanoTimeout;
for (; ; ) {
if (tryLock(lockKey,requestId)) {
return true;
}
nanoTimeout = deadLine - System.nanoTime();
if (nanoTimeout <= 0) {
return false;
}
}
}
@Override
public boolean unLock(String lockKey,String requestId) {
StringBuffer scriptBuffer = new StringBuffer();
scriptBuffer.append(" if ");
scriptBuffer.append(" redis.call('get',KEYS[1]) == ARGV[1] ");
scriptBuffer.append(" then ");
scriptBuffer.append(" return redis.call('del',KEYS[1]) ");
scriptBuffer.append(" else ");
scriptBuffer.append(" return 0 ");
scriptBuffer.append(" end");
//多线程环境,不能使用单例模式的jedis实例
Jedis jedis = new Jedis("127.0.0.1", 6379);
Object result = jedis.eval(scriptBuffer.toString(), Collections.singletonList(lockKey), Collections.singletonList(requestId));
if (RELEASE_SUCCESS.equals(result)) {
return true;
}
return false;
}
}
遇到的问题:
redis.clients.jedis.exceptions.JedisConnectionException: java.net.SocketException: Connection reset
产生的原因:在高并发环境下使用单例模式创建Jedis连接导致的
(3)利用zookeeper的实现
1)zookeeper的四种节点及其特性
A:PERSISTENT 持久化节点
B:PERSISTENT_SEQUENTIAL 顺序自动编号持久化节点,这种节点会根据当前已存在的节点自动加1
C:EPHEMERAL 临时节点,客户端session超时 这类节点就会被自动删除
D:EPHEMERAL_SEQUENTIAL 临时自动编号节点
2)zookeeper的第一种实现方式
这种实现方案:会出现一种惊群现象
流程图:
package com.roger.lock.impl;
import com.roger.lock.DistributeLock;
import com.roger.utils.ZkClientUtil;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import org.springframework.stereotype.Service;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
@Service("zkLockV1")
public class ZkLockV1 implements DistributeLock {
@Override
public void lock(String lockKey, String lockValue) {
if(!tryLock(lockKey,lockValue)){
//优雅的阻塞自己
waitForLock(lockKey);
//再次尝试获取锁,直到获取锁成功
lock(lockKey,lockValue);
}
}
@Override
public boolean tryLock(String lockKey, String lockValue) {
try {
ZkClientUtil.getInstance().createEphemeral(lockKey, lockValue);
}catch (ZkNodeExistsException e){
return false;
}
return true;
}
@Override
public boolean tryLock(String lockKey, String lockValue, long time, TimeUnit timeUnit) {
if(tryLock(lockKey,lockValue)){
return true;
}
if(time <= 0){
return false;
}
long nanoTimes = timeUnit.toNanos(time);
long deadlineTime = System.nanoTime() + nanoTimes;
for (;;){
if(tryLock(lockKey,lockValue)){
return true;
}
nanoTimes = deadlineTime - System.nanoTime();
if(nanoTimes <= 0){
return false;
}
}
}
@Override
public boolean unLock(String lockKey, String lockValue) {
String oldValue = ZkClientUtil.getInstance().readData(lockKey);
if(lockValue.equals(oldValue)){
return ZkClientUtil.getInstance().delete(lockKey);
}
return false;
}
private void waitForLock(String lockKey) {
CountDownLatch countDownLatch = new CountDownLatch(1);
//注册监听事件
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//当节点被删除后,唤醒阻塞中的线程,再次竞争共享资源
countDownLatch.countDown();
}
};
ZkClientUtil.getInstance().subscribeDataChanges(lockKey,iZkDataListener);
//注册完成后,判断节点是否依然存在
// 如果依然存在,则阻塞线程
if(ZkClientUtil.getInstance().exists(lockKey)){
try {
countDownLatch.await();
}catch (InterruptedException e){
}
}
//取消监听事件
ZkClientUtil.getInstance().unsubscribeDataChanges(lockKey,iZkDataListener);
}
}
3)zookeeper的另一种实现方案,避免出现惊群现象,使用创建临时顺序节点的方案
流程图
package com.roger.lock.impl;
import com.roger.lock.DistributeLock;
import com.roger.utils.ZkClientUtil;
import org.I0Itec.zkclient.IZkDataListener;
import org.I0Itec.zkclient.exception.ZkNodeExistsException;
import org.springframework.stereotype.Service;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 这种锁的使用方法不能是单例的
* 每个线程必须有自己的锁对象,即线程独享对象
*/
public class ZkLockV2 implements DistributeLock {
//当前节点
private String currentPath;
//上一个节点
private String previousPath;
@Override
public void lock(String lockKey, String lockValue) {
if(!tryLock(lockKey,lockValue)){
//优雅的阻塞自己
waitForLock(lockKey);
//再次尝试获取锁
lock(lockKey,lockValue);
}
}
@Override
public boolean tryLock(String lockKey, String lockValue) {
//如果lockKey节点不存在,则创建要给持久节点
//用来存数临时顺序节点
if(!ZkClientUtil.getInstance().exists(lockKey)){
try {
ZkClientUtil.getInstance().createPersistent(lockKey);
}catch (ZkNodeExistsException e){
//高并发情况下,可能有多个线程同时进入创建持久节点
//因此这里需要捕获异常,不需做任何处理
}
}
//如果currentPath为null,则该线程还没有进入阻塞状态
//即是第一次尝试获取锁
if(currentPath == null) {
//创建当前临时顺序节点
currentPath = ZkClientUtil.getInstance().createEphemeralSequential(lockKey + "/", lockValue);
currentPath = currentPath.substring(currentPath.lastIndexOf("/")+1);
}
List childNodeList = ZkClientUtil.getInstance().getChildren(lockKey);
Collections.sort(childNodeList);
String minNodePath = childNodeList.get(0);
if(minNodePath.equals(currentPath)){
return true;
}
previousPath = childNodeList.get(childNodeList.indexOf(currentPath) - 1);
return false;
}
@Override
public boolean tryLock(String lockKey, String lockValue, long time, TimeUnit timeUnit) {
if(tryLock(lockKey,lockValue)){
return true;
}
if(time <= 0){
return false;
}
long nanoTimes = timeUnit.toNanos(time);
long deadlineTime = System.nanoTime() + nanoTimes;
for (;;){
if(tryLock(lockKey,lockValue)){
return true;
}
nanoTimes = deadlineTime - System.nanoTime();
if(nanoTimes <= 0){
return false;
}
}
}
@Override
public boolean unLock(String lockKey, String lockValue) {
String oldValue = ZkClientUtil.getInstance().readData(lockKey + "/" + currentPath);
if(lockValue.equals(oldValue)){
return ZkClientUtil.getInstance().delete(lockKey + "/" + currentPath);
}
return false;
}
private void waitForLock(String lockKey) {
CountDownLatch countDownLatch = new CountDownLatch(1);
//注册监听事件
IZkDataListener iZkDataListener = new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
}
@Override
public void handleDataDeleted(String s) throws Exception {
//当节点被删除后,唤醒阻塞中的线程,再次竞争共享资源
countDownLatch.countDown();
}
};
ZkClientUtil.getInstance().subscribeDataChanges(lockKey + "/" + previousPath,iZkDataListener);
//注册完成后,判断节点是否依然存在
// 如果依然存在,则阻塞线程
if(ZkClientUtil.getInstance().exists(lockKey + "/" + previousPath)){
try {
countDownLatch.await();
}catch (InterruptedException e){
}
}
//取消监听事件
ZkClientUtil.getInstance().unsubscribeDataChanges(lockKey + "/" + previousPath,iZkDataListener);
}
}