分布式锁
在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁
阻塞锁使用一个互斥量来实现:
- 0代表其他进程在使用锁
- 1代表未锁定
可以用一个整数表示,或者也可以用某个数据是否存在来表示
数据库唯一索引
获得锁时向表中插入一条记录,释放锁时删除这条记录
- 锁没有失效时间,容易死锁
- 是非阻塞的,获取锁失败就报错
- 不可重入
redis setnx
1.获取锁的时候,对某个key执行setnx,加锁(如果设置成功(获得锁)返回1,否则返回0),并使用expire命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的value值为一个随机生成的UUID,通过此在释放锁的时候进行判断。
2.获取锁的时候还设置一个获取的超时时间(防止死锁),若超过这个时间则放弃获取锁。
3.释放锁的时候,通过UUID判断是不是该锁,若是该锁,则执行delete进行锁释放
实现
public class RedisLock {
private StringRedisTemplate template;
private static final String LOCK_KEY = "LOCK";
private String identifyValue;
public RedisLock(StringRedisTemplate template) {this.template = template;}
/**
* @param acquireTimeout 获取锁之前的超时时间
* @param expireTime 锁的过期时间
* @return
*/
public boolean lock(long acquireTimeout, long expireTime) {
// 获取锁的时间
long inTime = System.currentTimeMillis();
identifyValue = UUID.randomUUID().toString();
for (; ; ) {
// 判断获取锁是否超时
if (System.currentTimeMillis() - inTime >= acquireTimeout) {
return false;
}
// 通过setnx的方式来获取锁
if (template.opsForValue().setIfAbsent(LOCK_KEY, identifyValue, expireTime, TimeUnit.MILLISECONDS)) {
// 获取锁成功
return true;
}
// 获取锁失败,继续自旋
}
}
public void release() {
if (identifyValue == null){
throw new IllegalStateException("没有获取锁");
}
// 删除的时候验证value,必须确保释放的锁是自己创建的
if (!identifyValue.equals(template.opsForValue().get(LOCK_KEY))){
throw new IllegalStateException("锁的value不一致");
}
template.delete(LOCK_KEY);
}
}
与zookeeper比较
相对比来说Redis比Zookeeper性能要好,从可靠性角度分析,Zookeeper可靠性比Redis更好。因为Redis有效期不是很好控制,可能会产生有效期延迟
redis redlock
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用
计算获取锁消耗的时间,只有消耗的时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,才认为获取锁成功 如果获取锁失败,就到每个实例上释放锁
zookeeper临时节点
多个进程同时在zookeeper.上创建同一个相同的节点(/lock) , 因为zookeeper节点是唯一的,如果是唯一的话,那么同时如果有多个客户端创建相同的节点/lock的话,最终只有看谁能够快速的抢资源,谁就能创建/lock节点,创建节点不成功的进程,会注册一个监听事件,等节点被删除的时候,重新竞争这个锁 这个时候节点类型应该使用临时类型。
当一个进程释放锁后(关闭zk连接或者会话超时),临时节点会被删除,等待锁的其他进程会收到节点被删除的通知,这些等待的进程会重新参与到竞争
需要注意的是,要根据业务设置锁等待时间,避免死锁
实现
- 上锁
public void lock() {
// 尝试获取锁,如果成功,就真的成功了
if (tryLock()) {
System.out.println(Thread.currentThread().getName() + "获取锁成功");
// 否则等待锁
} else {
waitLock();
// 当等待被唤醒后重新去竞争锁
lock();
}
}
private boolean tryLock() {
try {
// 通过zk创建临时节点的成功与否来表示是否获得锁
zkClient.createEphemeral("/lock");
return true;
} catch (Exception e) {
return false;
}
}
private void waitLock() {
// 监听节点被删除的事件
zkClient.subscribeDataChanges("/lock", new IZkDataListener() {
@Override
public void handleDataDeleted(String s) throws Exception {
// 如果节点被删除,唤醒latch
if (latch != null) {
latch.countDown();
}
}
});
// 如果zk有lock这个锁
if (zkClient.exists("/lock")) {
// 在这里进行等待,直至被上面的事件监听唤醒
latch = new CountDownLatch(1);
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
// 等待完成删除所有监听事件,避免监听器堆积影响性能
zkClient.unsubscribeAll();
}
- 释放锁
public void release() {
if (zkClient != null) {
// 关闭zk客户端,临时节点也随之被删除,相当于释放锁,让其他人去竞争
zkClient.close();
System.out.println(Thread.currentThread().getName()+"释放锁完成");
}
}
zookeeper临时顺序节点
有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听排在自己前面的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁
zk锁 vs redis锁
- redis 分布式锁,其实需要自己不断去尝试获取锁,比较消耗性能
- zk 分布式锁,获取不到锁,注册个监听器即可,等待zk的通知
- redis如果客户端没有及时释放锁,会发生死锁
分布式Session
集群产生的问题
服务器集群后,因为session是存放在服务器上,客户端会使用同一个Sessionid在多个不同的服务器上获取对应的Session,从而会导致Session不一致问题
解决方案
- cookie代替session
- nginx将同一个ip的请求都转发到同一台服务器
- 使用数据库存储session
- 使用web容器的session同步
- 使用redis存储session
- 使用token或者jwt存储用户信息,需要时再去数据库或者cache查
SpringSession 使用redis存储session
- 依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
- 配置
@EnableRedisHttpSession
这时候,Session的存取都是通过redis来了