【Java面试高频-集合】- 读写的场景设计集合是怎么样?对于读多写少要如何设计的呢?对于读少写多又该如何设计呢?
发布日期:2021-06-29 15:36:45 浏览次数:2 分类:技术文章

本文共 3191 字,大约阅读时间需要 10 分钟。

Java集合-读写的场景

1 读多写少的场景

**CopyOnWrite的思想。**读数据不加锁,写数据的时候利用拷贝的副本来执行。

ReadWriteLock的思想也可以

那么 写线程现在把副本数组给修改完了,现在怎么才能让读线程感知到这个变化呢?

配合volatile关键字的使用,核心就是让一个变量被写线程给修改之后,立马让其他线程可以读到这个变量引用的最近的值。 所以一旦线程搞定了副本数组的修改之后,那么就可以用volatile写的方式,把这个副本数组赋值给volatile修饰的那个数组的引用变量了。只要一赋值给那个volatile修饰的变量,立马就会对读线程可见,大家就能看到最新的数组了。

那么要是多个线程要同时更新呢?那搞出来 多个副本会不会有什么问题?

不会,因为加入一个lock锁的机制,也就是同一时间只能有一个线程可以更新。

2 读少写多的场景

优化思路总结:

  • 给每个用户信息配一把锁,把用户信息存在数组中,array[drive_id]=driver_info.如果数据量不大,这种方案可行。如果数据量非常大,会非常耗内存;
  • 水平切分,按照driver_id进行哈希,比如对10000进行取模,则把司机信息切分成1w组,用1w个map来存放司机信息。面对大数据时这种方法非常适合;
  • 去掉锁,存放的时候仍然以KV的map来存放,但是value不是在单独的driver_info, 而是先给driver_info生成一个签名,然后将签名+driver_info分两步写到定长的内存中,作为value存放。取得时候,对value中的driver_info先生成签名,然后跟取出来的签名比较。如果一样,说明driver_info没有发生竟态写。

> [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XeEtQ9uj-1620869218912)(imgs\131.png)]

(1) 需求缘起

【业务场景】

有一类写多读少的场景:大部分请求是对数据进行修改,少部分请求对数据读取;

例子1: 例子1:滴滴打车,某个司机地理位置信息的变化(可能每几秒钟有一个修改),以及司机地理位置的读取(用户打车的时候查看某个司机的地理位置)。

void SetDriverInfo(long driver_id, DriverInfoi); // 大量请求调用修改司机信息,可能主要是GPS位置的修改

DriverInfo GetDriverInfo(long driver_id); // 少量请求查询司机信息

【底层实现】

具体到底层实现往往就是一个Map(本质上是一个定长key,定长value的缓存结构)来存储司机的信息,或者某个类型的计数。

【临界资源】

这个Map存储了所有信息,当并发读写访问时,它作为临界资源,在读写之前,一般要进行加锁操作,以司机信息为例:

void SetDriverInfo(long driver_id, DriverInfoinfo){
WriteLock (m_lock); Map= info; UnWriteLock(m_lock); } DriverInfo GetDriverInfo(long driver_id){
DriverInfo t; ReadLock(m_lock); t= Map; UnReadLock(m_lock); return t; }

【并发锁瓶颈】

假设滴滴有100w司机同时在线,每个司机没5秒更新一次经纬度状态,那么每秒就有20w次写并发操作。假设滴滴日订单1000w个,平均每秒大概也有300个下单,对应到查询并发量,可能是1000级别的并发读操作。

上述实现方案没有任何问题,但在并发量很大的时候(每秒20w写,1k读),锁m_lock会成为潜在瓶颈,在这类高并发环境下写多读少的业务仓井,如何来进行优化,是本文将要讨论的问题。

(2) 水平切分+锁粒度优化

上文中之所以锁冲突严重,是因为所有司机都公用一把锁,锁的粒度太粗(可以认为是一个数据库的“库级别锁”),是否可能进行水平拆分(类似于数据库里的分库),把一个库锁变成多个库锁,来提高并发,降低锁冲突呢?显然是可以的,把1个Map水平切分成多个Map即可:

void SetDriverInfo(long driver_id, DriverInfoinfo){
i= driver_id % N; // 水平拆分成N份,N个Map,N个锁 WriteLock (m_lock [i]); //锁第i把锁 Map[i]
= info; // 操作第i个Map UnWriteLock (m_lock[i]); // 解锁第i把锁}

每个Map的并发量(变成了1/N)和数据量都降低(变成了1/N)了,所以理论上,锁冲突会成平方指数降低。

分库之后,仍然是库锁,有没有办法变成数据库层面所谓的“行级锁”呢,难道要把x条记录变成x个Map吗,这显然是不现实的。

(3) Map变Array+最细锁粒度优化

假设driver_id是递增生成的,并且缓存的内存比较大,是可以把Map优化成Array,而不是拆分成N个Map,是有可能把锁的粒度细化到最细的(每个记录一个锁)。

void SetDriverInfo(long driver_id, DriverInfoinfo){
index= driver_id; WriteLock (m_lock [index]); //超级大内存,一条记录一个锁,锁行锁 Array[index]= info; //driver_id就是Array下标 UnWriteLock (m_lock[index]); // 解锁行锁}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-aKqyescg-1620869218914)(imgs\129.png)]

和上一个方案相比,这个方案使得锁冲突降到了最低,但锁资源大增,在数据量非常大的情况下,一般不这么搞。数据量比较小的时候,可以一个元素一个锁的(典型的是连接池,每个连接有一个锁表示连接是否可用)。

上文中提到的另一个例子,用户操作类型计数,操作类型是有限的,即使一个type一个锁,锁的冲突也可能是很高的,还没有方法进一步提高并发呢?

(3)把锁去掉,变成无锁缓存

【无锁的结果】

void AddCountByType(long type /*, int count*/){
//不加锁 Array[type]++; // 计数++ //Array[type] += count; // 计数增加count}

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ULZBPe5b-1620869218915)(imgs\130.png)]

如果这个缓存不加锁,当然可以达到最高的并发,但是多线程对缓存中同一块定长数据进行操作时,有可能出现不一致的数据块,这个方案为了提高性能,牺牲了一致性。在读取计数时,获取到了错误的数据,是不能接受的(作为缓存,允许cache miss,却不允许读脏数据)。

加上签名之后,不但缓存要写入定长value本身,还要写入定长签名(例如16bitCRC校验):

(1)线程1对缓存进行操作,对key想要写入value1,写入签名v1-sign;

(2)线程2对缓存进行操作,对key想要写入value2,写入签名v2-sign;

(3)如果不加锁,线程1和线程2对同一个定长区域进行一个并发的写操作,可能每个线程写成功一半,导致出现脏数据产生,最终的结果即不是value1也不是value2,而是一个乱七八糟的不符合预期的值value-unexpected,但签名,一定是v1-sign或者v2-sign中的任意一个;

画外音:16bit/32bit的写可以保证原子性。

(4)数据读取的时候,不但要取出value,还要像消息接收方收到消息一样,校验一下签名,如果发现签名不一致,缓存则返回NULL,即cache miss;

转载地址:https://codingchaozhang.blog.csdn.net/article/details/116736775 如侵犯您的版权,请留言回复原文章的地址,我们会给您删除此文章,给您带来不便请您谅解!

上一篇:【Java面试高频-集合】如何在10亿int型数,统计只出现一次的数字?
下一篇:【Java面试高频-集合】Java集合中的快速失败机制fail-fast和fail-safe是怎么样的?

发表评论

最新留言

路过按个爪印,很不错,赞一个!
[***.219.124.196]2024年04月06日 14时31分52秒