前言;本文是在服务器中已安装redis 和 zookeeper 服务的前提下进行的;
背景:在并发编程中,我们使用锁来保证多线程对于临界资源访问的安全性,在同一个进程中我们可以使用synchronized,Lock 等来保证;那么在不同的进程中我们怎么来保证多个线程对于临界资源访问的安全性;此时就需要一个全局的地方来记录锁,所有进程中的线程,都可以向一个地方去获取和释放锁;
1 分布式锁实现:
要想实现分布式锁:
(1)要有一个全局的地方,使得多个进程下的多个线程都可以来此记录锁;
(2)要有获取锁的机制,如获取成功直接返回true失败返回false;或者一直阻塞等待获取锁;
(3)要有获取锁成功后但是改线程死掉时 释放锁的机制,不然资源会一直占用;
(4)要有释放锁的机制;
2 分布式锁使用:
2.1 采用redis 的setNx 实现分布式锁:
String redisLockKey="自己定义的 redis lock key";
String redisKey="自己定义的缓存数据项 redis key";
if (!redisUtil.hasKey(redisKey)) {
// 此时缓存中无业务数据,需要获取锁,然后缓存业务数据
// 数据加载并缓存--获取锁(暂不考虑续期),10s后失效 设置锁的失效时间防止线程挂掉后锁资源无法释放
while (!redisUtil.tryLock(redisLockKey, 10, TimeUnit.SECONDS)) {
try {
// 获取锁失败后,睡眠1s 然后继续尝试去获取锁
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
// 相应线程的中断,并返回
return ;
}
}
// 此时获取到了锁
if (!redisUtil.hasKey(redisKey)) {
// 双重判断--防止线程重复加载
try {
// 获取数据并进行缓存
// doSomething
} catch (Exception ex) {
log.error("获取住宅批次信息异常,cause:{}", ExceptionFormatUtil.buildErrorMessage(ex));
} finally {
// 释放锁
redisUtil.unlock(redisLockKey.toString());
}
} else {
// 释放锁
redisUtil.unlock(redisLockKey.toString());
}
}
redisUtil 类:
/**
* 尝试获取锁
*
* @param key
* @param timeout 秒为单位
* @return
*/
public boolean tryLock(String key, long timeout, TimeUnit timeUnit) {
if (setIfAbsent(key, "1")) {
expire(key, timeout, timeUnit);
return true;
} else {
return false;
}
}
/**
* 释放锁
*
* @param key
* @return
*/
public boolean unlock(String key) {
delete(key);
return true;
}
2.2 采用zookeeper 实现分布式锁:
2.2.1 定义获取锁工具类:
ZookeeperClient:
@Slf4j
@Component
public class ZookeeperClient {
@Autowired
private CuratorFramework curatorFramework;
@Getter
@Setter
private String zookeeperLockPath = "/springboot_zk_lock/";
public <T> T lock(AbstractZookeeperLock<T> mutex) {
String path = this.getZookeeperLockPath() + mutex.getLockPath();
//创建锁对象
InterProcessMutex lock = new InterProcessMutex(curatorFramework, path);
boolean success = false;
try {
try {
//获取锁
success = lock.acquire(mutex.getTimeout(), mutex.getTimeUnit());
} catch (Exception e) {
throw new RuntimeException("obtain lock error " + e.getMessage() + ", path " + path);
}
if (success) {
return (T) mutex.execute();
} else {
return null;
}
} finally {
try {
if (success) {
lock.release(); //释放锁
}
} catch (Exception e) {
log.error("release lock error {}, path {}", e.getMessage(), path);
}
}
}
}
AbstractZookeeperLock:
public abstract class AbstractZookeeperLock<T> {
private static final int TIME_OUT = 5;
public abstract String getLockPath();
public abstract T execute();
public int getTimeout() {
return TIME_OUT;
}
public TimeUnit getTimeUnit() {
return TimeUnit.SECONDS;
}
}
2.2.2 测试:
TestLock:
public abstract class TestLock<String> extends AbstractZookeeperLock<String> {
private static final java.lang.String LOCK_PATH = "test_";
@Getter
private String lockId;
public TestLock(String lockId) {
this.lockId = lockId;
}
@Override
public java.lang.String getLockPath() {
return LOCK_PATH + this.lockId;
}
}
ZkLockTest:
@SpringBootTest
public class ZkLockTest {
@Autowired
private ZookeeperClient zookeeperClient;
@Test
public void zookeeperLockTest() {
String lockId = "123123";
String result = zookeeperClient.lock(new TestLock<String>(lockId) {
@Override
public String execute() {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return this.getLockId();
}
});
if (result == null) {
System.out.println("执行失败");
} else {
System.out.println("执行成功");
}
}
}
3 两种分布式锁对比:
(1) redis 做分布式锁,借用了setNx 命令,只用当库中key 不存在时才进行存入,否则不允许存入,在业务完成后通过删除key 来实现释放锁;
(2)zookeeper 借用了本身临时节点特性来实现,只用当客户端创建临时节点成功时,代表获取到锁,在业务完成,退出链接后,zookeeper 删除该临时节点从而释放锁;
(3)在使用redis 做分布式锁时,在业务上要对key 设置失效时间从而避免死锁,而setNx 和设置过期时间是两条命令,无法保证原子性,所以必要的时候,使用lua 脚本来保证命令行的原子性;因为设置了key的过期时间,就要小心因为时间到了,但是业务没有执行完毕的情况,这里涉及锁的续期需求;
(4)zookeeper 做分布式锁时通过创建临时节点来获取锁,断开链接后临时节点会被删除,所以不需要额外考虑其失效时间和所得续期;
(5)redis 作为纯内存,数据的访问要比zookeeper 高效很多,但是redis 集群属于AP 模型,保证数据的最终一直性,而不强调强一致性;
redis为了保证高可用,主从复制过程是异步的,也就是说,当客户端向master写了一个数据,master提交完就给你飞吻了,而不管slaver是否已经同步完成数据,这样写数据和同步数据各玩各的,同步数据过程中不影响写数据,这样叫做高可用,但也带来了以下两个问题:
问题1,主从复制丢数据
C1向master申请到了锁a,master在同步数据的过程中挂掉了,此时slaver还没有拿到锁a的数据,主备切换,slaver升级成master1,C2向master1申请锁a,申请成功,此时C1和C2都拿到锁,锁失效了。
问题2,脑裂
C1向master申请到了锁a,master网络出问题了,sentinel收不到master的心跳,于是将slaver选举成master1,但此时还有些C端能够访问到master,假如此时C2能够访问到master,但C3访问的却是master1,于是C2在master上能够申请到锁b,C3在master1上也能够申请到锁b
(6)zookeeper 集群是CP模型,保证强一致性,不管谁被选举成为leader 数据是一致的;
(7)redis 和zookeeper 都是不可重入锁;
4 总结:
redis和zk分布式锁的区别,关键就在于高可用性和强一致性的选择,redis的性能高于zk太多了,可在一致性上又远远不如zk。