分布式场景下的一些思考

最近在研究Redission在分布式场景下的一些应用,系统的梳理了一下分布式场景下的一些知识点;

一、多线程

在分布式场景下,离不开不同服务器不同进程之间的通信;进程之间通信交换数据要么通过在堆内存共享数据方式,要么通过消息通知的方式;说到进程,在多核CPU处理器,实际上是通过多线程轮流切换,竞争CPU资源的方式来工作。这种通过分配时间片的方式,协调多线程处理。回溯到线程的生命周期,线程生命周期有6种状态:
分布式场景下的一些思考
分布式场景下的一些思考

在阻塞状态,有看到一个synchronized关键字,这个想必大家都清楚,这是为了能够在多线程环境下让同一时刻只有一个线程共享资源的一种悲观加锁方式,讲到这里,必须梳理一下多线程环境下锁的一些知识

Java中的锁

锁是用来控制多个线程访问共享资源的方式,一般来说,一个锁能防止多个线程同时访问共享资源,当然也有些锁可以允许多个线程并发访问共享资源,比如读写锁。在Java中,一般常用的是Lock接口锁,相比java5之前的synchronized关键字只能隐式的获取和释放锁,Lock更加灵活,它提供的synchronized关键字不具备的特性如下:
分布式场景下的一些思考
后面我们会谈到实现Lock接口的具体锁,比如重入锁、分布式锁、读写锁等,其实这些锁都离不开一个基础组件同步器AbstractQueuedSynchronizer,下面先聊一下AQS的一些原理:
AQS提供模板的方式,继承者只需要实现同步状态的管理就可以了,AQS大致分为3类,独占式的获取和释放锁、共享式的获取和释放锁和查询同步队列中的等待线程情况。同步器对于状态的修饰是volatile,根据JMM内存模型的happen-before原则,写happen-before读,保证线程的安全

重入锁

ReentrantLock,支持重进入的锁,表示该锁支持一个线程对资源的重复加锁,重进入是指任意线程在获取到锁之后能够再次获取到该锁而不会被锁阻塞

该特性实际上解决2个问题:(1) 线程再次获取锁:通过判断当前线程是否为获取锁的线程来
决定获取操作是否成功,如果是获取锁的线程再次请求,则将同步状态值进行增加并返回
true (2) 锁的最终释放:如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)方法必须返回false,而只有同步状态完全释放了,才能返回true

重入锁存在获取锁的公平和非公平性,对于非公平锁,只要CAS设置同步状态成功,就表示当前线程获取了锁,而公平锁不同,增加了同步队列当前节点是否有前驱节点的判断;公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量

读写锁

读写锁和其他排他锁不同,它允许同一时刻多个读线程访问,但是在写线程访问时,所有的读线程和其他线程均被阻塞。读写锁维护了一对锁,一个读锁一个写锁,通过分离读锁和写锁,使得并发性比一般的排他锁性能要高;Java并发包提供读写锁的实现是ReentrantReadWriteLock

读写锁实际上在维护同步状态时,需要在同步状态上维护多个读线程和多个写线程的状态,在一个整型变量上维护多种状态,实际上是按位进行切割,高16为表示读状态,低16位表示写状态

在分布式场景下,我们不得不提到分布式锁

分布式锁

分布式锁是为了解决在分布式场景下,多个线程对共享资源的互斥访问,从而保证线程能够正确的返回结果;常见的分布式锁的实现方式通过redis来实现

对于分布式锁的实现思路,只要算法具备3个特性就可以满足最低保障的分布式锁

  • 安全属性(Safety property): 独享(相互排斥)。在任意一个时刻,只有一个客户端持有锁
  • 活性A(Liveness property A): 无死锁。即便持有锁的客户端崩溃(crashed)或者网络被分裂(gets partitioned),锁仍然可以被获取
  • 活性B(Liveness property B): 容错。 只要大部分Redis节点都活着,客户端就可以获取和释放锁

在redis命令中setnx,当key不存在,才设置成功,返回1,如果存在,则失败返回0,可以满足特性1,通过expire命令,设置key的TTL的时间,可以满足特性2,释放锁的时候,通过del命令删除key;对于特性3往往在实现过程中没有解决这个问题;这里不得不谈到redis的部署方式,redis部署方式由单节点部署、主从方式、哨兵方式、以及集群方式;在生产环境,容灾部署方式比较多的是哨兵部署方式,通过转移故障节点,从节点竞升为master节点。这种方式存在一个安全问题:

客户端A从master获取到锁 ;在master将锁同步到slave之前,master宕掉了。 slave节点被晋级为master节点;客户端B取得了同一个资源被客户端A已经获取到的另外一个锁。安全失效!

所以更加安全的分布式算法一般通过红锁算法RedLock来实现,在Redis的高级应用Redission来实现,具体请看http://redis.cn/topics/distlock.html

在应用场景种涉及到对有限公共资源的访问,不得不提及限流,这种线程安全访问方式通过信号量来控制,java安全包concurrent包下面的Semaphore;主要提供的方法:acquire()、release()、tryAcquire()、tryRelease()

更多分布式场景下锁的一些分类可以参考:https://github.com/redisson/redisson/wiki/8.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E5%92%8C%E5%90%8C%E6%AD%A5%E5%99%A8#81-%E5%8F%AF%E9%87%8D%E5%85%A5%E9%94%81reentrant-lock

并发容器集合

这里我只想聊一下并发容器中的阻塞队列,在大家使用线程池的时候,离不开阻塞队列的选取;

阻塞队列是增加2个附加操作的队列,阻塞插入和阻塞移除;阻塞插入意思是当阻塞队列满时,队列会阻塞插入元素的线程,直到队列不满;阻塞移除的意思是当队列为空时,获取元素的线程会等待队列非空;下面列举了阻塞队列提供的4中常见的附加操作

分布式场景下的一些思考

JDK一共提供了7个阻塞队列:数组结构组成的阻塞队列、链表组成的有界阻塞队列、支持优先级的无界阻塞队列、同步阻塞队列、链表组成的无界双端阻塞队列、一个链表组成的无界阻塞队列、优先级队列实现的无界阻塞队列

阻塞队列实现原理就是生产消费模式,数组阻塞队列生产消费线程用的是同一个重入锁,而链表阻塞队列生产消费线程使用的2个重入锁,所以链表阻塞队列性能优于数组阻塞队列;对于并发量比较小的情况下,同步队列的性能优于其他2个阻塞队列

分布式场景下的一些思考

分布式集合

基于Redis的Redission实现的RMap Java对象实现了java.util.concurrent.ConcurrentMap接口和java.util.Map接口 ,该数据结构受限于Redis的存储容量,redis存储数据最大为512M;详细:
https://github.com/redisson/redisson/wiki/7.-%E5%88%86%E5%B8%83%E5%BC%8F%E9%9B%86%E5%90%88#72-%E5%A4%9A%E5%80%BC%E6%98%A0%E5%B0%84multimap

二、Redis的部署方式

转载一篇文章,讲述的比较详细
https://my.oschina.net/zhangxufeng/blog/905611