分布式锁
在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁
阻塞锁使用一个互斥量来实现:
- 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来了