分布式锁

Java 提供了两种内置的锁的实现,一种是由JVM实现的synchronized和JDK提供的 Lock,当你的应用是单机或者说单进程应用时,可以使用synchronized或Lock来实现锁。但是,当应用涉及到多机、多进程共同完成时,那么这时候就需要一个全局锁来实现多个进程之间的同步。

基于数据库分布式锁

可以采用基于数据库的分布式锁,例如基于MySQL锁表:该实现方式完全依靠数据库唯一索引来实现。当想要获得锁时,就向数据库中插入一条记录,释放锁时就删除这条记录。如果记录具有唯一索引,就不会同时插入同一条记录。

但是,这种方式存在以下几个问题:

  • 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
  • 只能是非阻塞锁,插入失败直接就报错了,无法重试。
  • 不可重入,同一线程在没有释放锁之前无法再获得锁。
  • 采用乐观锁增加版本号

根据版本号来判断更新之前有没有其他线程更新过,如果被更新过,则获取锁失败。

基于Redis 分布式锁

基于SETNX、EXPIRE

使用SETNX(set if not exist)命令插入一个键值对时,如果key已经存在,那么会返回 False,否则插入成功并返回True。因此客户端在尝试获得锁时,先使用SETNX向redis中插入一个记录,如果返回True表示获得锁,返回False表示已经有客户端占用锁。

EXPIRE可以为一个键值对设置一个过期时间,从而避免了死锁的发生。

RedLock算法

ReadLock 算法使用了多个redis实例来实现分布式锁,这是为了保证在发生单点故障时还可用。

尝试从 N 个相互独立redis实例获取锁,如果一个实例不可用,应该尽快尝试下一个。

计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。

如果锁获取失败,会到每个实例上释放锁。

Zookeeper分布式锁

Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。

Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。

img

Zookeeper有三类节点类型:

  • 永久节点: 不会因为会话结束或者超时而消失;
  • 临时节点: 如果会话结束或者超时就会消失;
  • 有序节点: 会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推。

监听器 为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。

zookeeper分布式锁实现方式:

  1. 创建一个锁目录 /lock。在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推。
  2. 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
  3. 执行业务代码,完成后,删除对应的子节点。

会话超时

如果一个已经获得锁的会话超时了,因为创建的是临时节点,因此该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库分布式锁的死锁问题。

羊群效应

在步骤二,一个节点未获得锁,需要监听监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知,而我们只希望它的下一个子节点收到通知。

基于etcd分布式锁

Etcd 支持 Revision 机制,那么对于同一个 lock,即便有多个客户端争夺(本质上就是 put(lockName, value) 操作),Revision 机制可以保证它们的 Revision 编号有序且唯一,那么,客户端只要根据 Revision 的大小顺序就可以确定获得锁的先后顺序,从而很容易实现公平锁。

参考: etcd:一款比Redis更骚的分布式锁的实现方式!用它 - 知乎 (zhihu.com)

分布式Session

如果不做任何处理的话,用户将出现频繁登录的现象,比如集群中存在 A、B 两台服务器,用户在第一次访问网站时,Nginx 通过其负载均衡机制将用户请求转发到 A 服务器,这时 A 服务器就会给用户创建一个 Session。当用户第二次发送请求时,Nginx 将其负载均衡到 B 服务器,而这时候 B 服务器并不存在 Session,所以就会将用户踢到登录页面。这将大大降低用户体验度,导致用户的流失,这种情况是项目绝不应该出现的。

Session复制

session复制是通过对web服务器(例如Tomcat)进行搭建集群,将session在服务器集群之间进行复制。session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多。

优点

  • 服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单。
  • Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可。

缺点

  • session同步的原理是在同一个局域网里面通过发送广播来异步同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况。

Session绑定

是指通过负载均衡的策略,让客户端每次都映射访问到同一台服务器上,这样就不会出现找不到Session的问题了。

例如,在nginx中,可以采用ip_hash的负载均衡策略,让同一个ip的客户端访问一致的服务器。

优点

  • 配置简单

缺点

  • 容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失
  • 前端不能有负载均衡,如果有,session绑定将会出问题

客户端存储

直接将信息存储在cookie中 cookie是存储在客户端上的一小段数据,客户端通过http协议和服务器进行cookie交互,通常用来存储一些不敏感信息。

优点

  • 实现简单

缺点

  • 数据存储在客户端,存在安全隐患
  • cookie存储大小、类型存在限制
  • 数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销

基于Redis的Session存储

基于Redis的Session存储方案是指将Session信息存储到Redis中,应用服务器通过访问Redis缓存来获取Session数据。当然,也可以使用数据库作为Session存储的介质。

优点

  • 这是企业中使用的最多的一种方式
  • spring为我们封装好了spring-session,直接引入依赖即可
  • 数据保存在redis中,无缝接入,不存在任何安全隐患
  • redis自身可做集群,搭建主从,同时方便管理

缺点

  • 多了一次网络调用,web容器需要向redis访问