主流分布式锁实现基本都是基于redis或zookeeper,下面是它的三种实现方式和对比:
基于Redis
1)单机版:
加锁:set(key,threadId,30,NX)(2.6以上支持)
解锁: 不可以直接del,因为有可能解锁之前锁已经过期了,然后是别人上的锁,所以上面的threadId起作用了,可以先验证threadId是否相同是否是自己的锁,可以用lua脚本保持原子性如下:
if redis.call("get",KEYS[1])==ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
上面的版本还不算完美,还有一种情况,有可能A线程的锁过期了,但A还在执行,此时又有另一个B线程加上锁了,这个时候就会出现同时有两个线程在执行,所以更完美的写法是A线程在执行的时候,同时需要给锁续期,可以启用一个守护线程给A的锁续期,完整的代码如下:
@Service
public class RedisLockService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
private ReentrantLock reentrantLock = new ReentrantLock();
public void lock(String lockKey,String owner) {
boolean r = false;
do {
long s = System.currentTimeMillis();
reentrantLock.lock();//优化,避免高并发下同时大量线程空转消耗cpu资源,影响了整体的性能
r = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, owner, 30, TimeUnit.SECONDS);
if (r) {
Thread thread = new Thread(() -> {
long e = System.currentTimeMillis();
for (;;) {
if(Thread.interrupted()) {
break;
}
try {
stringRedisTemplate.expire(lockKey, 30, TimeUnit.SECONDS);
} catch (Exception e1) {
//下面unlock方法中thread.interrupt();会导致stringRedisTemplate内部捕获到抛出异常
break;
}
LockSupport.parkNanos(10000000000l);
}
});
thread.setDaemon(true);
thread.start();
{
System.out.println("我拿到了锁:" + lockKey + ",owner:" + owner);
unlock(lockKey,owner,thread);
}
} else {
}
}while (!r);
}
DefaultRedisScript<List> getRedisScript = null;
public RedisLockService() {
getRedisScript = new DefaultRedisScript<List>();
getRedisScript.setResultType(List.class);
//getRedisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("luascript/lock.lua")));
getRedisScript.setScriptText("if redis.call(\"get\",KEYS[1])==ARGV[1] then\n" +
" return redis.call(\"del\",KEYS[1])\n" +
"else\n" +
" return 0\n" +
"end");
}
public void unlock(String lockKey,String owner,Thread thread) {
try {
thread.interrupt();
stringRedisTemplate.execute(getRedisScript, Arrays.asList(lockKey),owner);
reentrantLock.unlock();
} catch (Exception e) {
e.printStackTrace();
}
}
}
2)redLock红锁
redLock是redis官方给出的分布式锁解决方案,redlock的算法描述就放在Redis的官网上:https://redis.io/topics/distlock,解决上面单机可靠性差(如:宕机)的问题,如果主从模式下中主服务宕机了,而主服务器没有把锁信息同步到从服务器或者同步到从服务器后过期了都会有问题,**redLock的原理是使用n台单个redis服务器,尝试向每一台redis服务器加锁,如果有n/2+1个结点成功了,则表明此次加锁成功,否则失败。某一台宕机不会影响加锁,或直接加锁失败。解锁向每台服务器发出解锁操作.
使用redisson开源工具实现redLock:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.3.2</version>
</dependency>
首先引入上面的jar包
@Configuration
public class RedisConfig {
@Bean
public RedissonClient redissonRed1(){
Config config = new Config();
config.useSingleServer().setAddress("192.168.0.109:6379").setDatabase(0);
return Redisson.create(config);
}
@Bean
public RedissonClient redissonRed2(){
Config config = new Config();
config.useSingleServer().setAddress("192.168.0.110:6379").setDatabase(0);
return Redisson.create(config);
}
@Bean
public RedissonClient redissonRed3(){
Config config = new Config();
config.useSingleServer().setAddress("192.168.0.111:6379").setDatabase(0);
return Redisson.create(config);
}
}
配置三台redis地址
@Service
public class RedLock {
@Autowired
private RedissonClient redissonRed1;
@Autowired
private RedissonClient redissonRed2;
@Autowired
private RedissonClient redissonRed3;
public void lock(String lockKey) {
// RLock默认设置key 超时时间30秒,过10秒再延时
RLock rLock1 = redissonRed1.getLock(lockKey);
RLock rLock2 = redissonRed2.getLock(lockKey);
RLock rLock3 = redissonRed3.getLock(lockKey);
RedissonRedLock rLock = new RedissonRedLock(rLock1,rLock2,rLock3);
rLock.lock();
try {
System.out.println("获得锁:"+lockKey);
} finally {
rLock.unlock();
}
}
}
分别向三台redis加锁,rLock.lock()返回表示有至少3/2+1=2台结点成功了
基于zookeeper
zookeeper可以实现多种类型的锁,公平锁,非公平锁,读写锁等。zookeeper相比redis功能更强大,更灵活。
1)公平锁:
1.基于CLH队列实现,当前节点监听前面节点的状态
2.Sequential EPHEMERAL Node 序列化的临时节点,随着client的session消失而删除
3.watch 监听节点删除事件
package com.zookeeper.lock;
import org.apache.zookeeper.*;
import org.apache.zookeeper.data.Stat;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.locks.LockSupport;
public class ZkLock {
public String lockName;
public static byte[] lockValue = "0".getBytes();
public ZkLock(String lockName) {
this.lockName = lockName;
}
public String lock(ZooKeeper zooKeeper) throws Exception {
String path = zooKeeper.create("/"+lockName,lockValue, ZooDefs.Ids.OPEN_ACL_UNSAFE,CreateMode.EPHEMERAL_SEQUENTIAL);
while(true) {
List<String> locks = zooKeeper.getChildren("/", false, new Stat());
Collections.sort(locks);
int i = locks.indexOf(path.substring(1));
if (i == 0) {
break;
} else {
Thread thread = Thread.currentThread();
//System.out.println("我是:"+path+",我在监听:"+"/" + locks.get(i - 1));
Stat stat = zooKeeper.exists("/" + locks.get(i - 1), new WatcherExists(zooKeeper, path, thread));
if(stat == null) {
//注意:这里必须要判断stat==null,是因为有可能前面的节点释放锁很快,在监听之前就已经删了,所以一定要看前面节点是否存在
zooKeeper.removeAllWatches("/" + locks.get(i - 1), Watcher.WatcherType.Any,false);
System.out.println("前面的节点不存在");
continue;
}
LockSupport.park();
//System.out.println(path + "被唤醒了");
}
}
return path;
}
class WatcherExists implements Watcher{
private ZooKeeper zooKeeper;
private String path;
private Thread thread;
public WatcherExists(ZooKeeper zooKeeper, String path, Thread thread) {
this.zooKeeper = zooKeeper;
this.path = path;
this.thread = thread;
}
@Override
public void process(WatchedEvent event) {
//System.out.println(event);
switch (event.getType()) {
case NodeDeleted:
try {
List<String> locks = null;
locks = zooKeeper.getChildren("/", false);
Collections.sort(locks);
int i = locks.indexOf(path.substring(1));
if(i == 0) {
LockSupport.unpark(thread);
}
} catch (KeeperException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
break;
}
}
}
public static void main(String[] args) throws Exception {
final ZkLock zkLock = new ZkLock("lockk");
int size = 20;
Thread[] threads = new Thread[size];
for(int i = 0;i<size;i++) {
final int j = i;
threads[i] = new Thread(()->{
/* if(j != 0) {
try {
threads[j-1].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}*/
try {
ZooKeeper zooKeeper = ZkConnection.getZookeeper();
String path = zkLock.lock(zooKeeper);
//System.out.println(path+":::::"+j);
zkLock.unlock(zooKeeper,path);
zooKeeper.close();
} catch (Exception e) {
e.printStackTrace();
}
});
threads[i].start();
}
for(int i = 0;i<size;i++) {
threads[i].join();
}
}
private void unlock(ZooKeeper zooKeeper,String path) {
try {
zooKeeper.delete(path,0);
//System.out.println("释放锁:"+path);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (KeeperException e) {
e.printStackTrace();
}
}
}
2)非公平锁:
类似redis实现
公平锁锁竞争:基于CLH自旋锁队列排队等待获取锁,每次释放锁发送event事件,只发送给后面的节点,只会唤起单个client。
非公平锁竞争: 所有节点监听同一个节点锁状态,每次释放锁event会发送给所有节点,引起所有client竞争
三种分布式锁性能测试对比
第一组:100线程,循坏执行10次**
zookeeper:
单机redis:
3台redis组成redlock:
第二组:200线程,循坏执行10次
zookeeper:
单机redis:
3台redis组成redlock:
第三组:500线程,循环执行10次
zookeeper:
单机redis:
3台redis组成redlock:
结论:
性能:redis>redlock>zookeeper
可靠性: zookeeper>redlock>redis
复杂度:redis>redlock>zookeeper
zookeeper还可以实现读写锁,公平非公平锁等
使用场景:
高并发下推荐还是用redis,一个字,快!可靠性不要求那么强的话选择单机redis。zookeeper并发不高的情况下也可,而且它比较灵活,可以实现公平锁,读写锁之类不同的锁(能按照你能想到的方式)。还是那句话,场景决定一切,多做测试!