无锁缓存,每秒10万并发,究竟如何实现?

有一类业务场景:

(1)高吞吐量,每秒要处理海量请求;

(2)写多读少,大部分请求是对数据进行修改,少部分请求对数据进行读取;

这类业务,有什么实现技巧么?

 

接下来,一起听我从案例入手,娓娓道来。

 
快狗打车场景举例
(1)司机地理位置信息会随时变化,可能每几秒钟地理位置要修改一次
(2)用户打车的时候查看某个司机的地理位置,查询地理位置的频率相对较低
 
这里要用到两个接口
(1)大量修改司机信息:
void SetDriverInfo(long driver_id, DriverInfo info);
(2)相对少量查询司机信息:
DriverInfo GetDriverInfo(long driver_id); 
这一类业务,一般怎么实现呢?
具体到底层的实现,往往是一个Map内存缓存
(1)查询key定长,例如:司机ID;
(2)返回value也定长,例如:司机实体序列化后的二进制串;
 
即,类似这样的一个kv缓存结构:
Map
这个kv内存缓存是一个临界资源,对它的并发访问,有什么注意事项么?
临界资源的访问,需要注意加读写锁,实施互斥
 
以下,是加锁写入的伪代码:

void SetDriverInfo(long driver_id, DriverInfo info){

         WriteLock (m_lock);

         Map= info;

         UnWriteLock(m_lock);

}

画外音:假设info已经序列化。
 
以下,是加锁读取的伪代码:

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会成为潜在瓶颈,导致Map访问效率极低。
 

有什么潜在的优化方法么?

锁冲突之所以严重,是因为整个Map共用一把锁,锁的粒度太粗。

画外音:可以认为是一个数据库的“库级别锁”。

是否可能进行水平拆分,来降低锁冲突呢?

答案是肯定的。

画外音:类似于数据库里的分库,把一个库锁变成多个库锁,来提高并发,降低锁冲突。

 

我们可以把1个Map水平切分成N个Map

void SetDriverInfo(long driver_id, DriverInfo info){

         i = driver_id % N; // 水平拆分成N份,N个Map,N个锁

         WriteLock (m_lock[i]);  //锁第i把锁

         Map[i]= info;  // 操作第i个Map

         UnWriteLock (m_lock[i]); // 解锁第i把锁

}

如此优化,能否提高性能?
(1)一个Map变成了N个Map,每个Map的并发量,变成了1/N
(2)同时,每个Map的数据量,变成了1/N
所以理论上,锁冲突会成平方指数降低,性能会提升。

有没有可能,进一步细化锁粒度,一个元素一把锁呢?

答案也是肯定的。

画外音:可以认为是一个数据库的“库级别锁”,优化为“行级别锁”。

 

不妨设driver_id是递增生成的,并且假设内存比较大,此时可以把Map优化成Array,并把锁的粒度细化到最细的,每个司机信息一个锁:

void SetDriverInfo(long driver_id, DriverInfo info){

         index = driver_id;

         WriteLock (m_lock[index]);  //超级大内存,一条记录一个锁,锁行锁

         Array[index]= info; //driver_id就是Array下标

         UnWriteLock (m_lock[index]); // 解锁行锁

}

无锁缓存,每秒10万并发,究竟如何实现?
这个方案使得锁冲突降到了最低,但锁资源大增,在数据量非常大的情况下,内存往往是装不下的。
画外音:数据量比较小的时候,可以一个元素一把锁,典型的是连接池,每个连接用一把锁表示连接是否可用。
 
还没有方法进一步降低锁冲突,提升并发量呢?

写多读少的业务,有一种优化方案无锁缓存,将锁冲突降低到。

无锁缓存,可能存在什么问题?
如果缓存不加锁,读写吞吐量可以达到极限,但是多线程对缓存中同一块定长数据进行写操作时,有可能出现不一致的脏数据
 
这个方案为了提高性能,牺牲了一致性。
 
读取时,获取到了错误的数据,是不能接受的
画外音:作为缓存,允许cache miss,却不允许读脏数据。
 
脏数据是如何产生的?
不加锁,在多线程并发写时,可能出现以下情况:
无锁缓存,每秒10万并发,究竟如何实现?
(1)线程1对缓存进行操作,对key想要写入value1
2)线程2对缓存进行操作,对key想要写入value2
3)不加锁,线程1和线程2对同一个定长区域进行一个并发的写操作,可能每个线程写成功一半,导致出现脏数据产生,最终的结果即不是value1也不是value2,而是一个乱七八糟的不符合预期的值value-unexpected
 
如何解决上述问题呢?
本质上,这是一个数据完整性问题。
并发写入的数据分别是value1value2,读出的数据是value-unexpected,数据被篡改,这本质上是一个数据完整性的问题。
 
通常如何保证数据的完整性呢?
 
例如:运维如何保证,从中控机分发到上线机上的二进制没有被篡改?
md5。
又例如:即时通讯系统中,如何保证接受方收到的消息,就是发送方发送的消息?
发送方除了发送消息本身,还要发送消息的签名,接收方收到消息后要校验签名,以确保消息是完整的,未被篡改。
 
“签名”是一种常见的保证数据完整性的方案。
 
加入“签名”保证数据的完整性之后,读写流程需要如何升级?
无锁缓存,每秒10万并发,究竟如何实现?
加上签名之后,不但缓存要写入定长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
 
当然,对应到司机地理位置,除了内存缓存之前,肯定需要timer对缓存中的数据定期落盘,写入数据库,如果cache miss,可以从数据库中读取数据。
 
巧不巧秒?
 

总结

当业务满足:
(1)高并发
(2)写多读少
(3)定长value
时,可以用以下方法来提升吞吐量:
(1)水平拆分来降低锁冲突;
思路单库变多库。
2)Map转Array的方式来最小化锁冲突,一条记录一个锁;
思路库锁变行锁。
 
3)无锁,最大化并发;
思路行锁变无锁,完整性与性能的折衷。
 
4)通过签名的方式保证数据的完整性,实现无锁缓存;
思路写时写签名,读时校验签名。
 
如果你喜欢本文,大概率会喜欢这个架构训练营,欢迎一起来玩。
无锁缓存,每秒10万并发,究竟如何实现?
扫码,一起玩架构,学别处没有的知识
思路比结论重要,希望大家有收获,谢
阅读原文,更多干货。

发布者:糖太宗,转载请注明出处:https://www.qztxs.com/archives/science/technology/6530

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022年5月14日 上午12:50
下一篇 2022年5月14日 上午12:53

相关推荐

  • 野路子企业安全架构设计(转发)

      首先,知道业务有哪些,有多少资产,然后是数据的流向,知道要分析哪些流量。想要发现什么,阻止什么。其实Google的OKR也可以在这里应用。例如,安全架构的OKR可以这样: O: 发现入侵者,减缓或者阻断攻击,以及取证。   KR: 资产管理系统 ——— 19% KR: 入侵检测系统 ——— 19% KR: 日志收集系统 ——— 19%...

    2022年5月27日
    2100
  • IAST测试靶场搭建

    0x00 前言 测试用例的选择尽量贴近真实业务使用的场景,覆盖常见的高危漏洞,以及各类请求形式。用于测试IAST产品支持检测漏洞类型、检出率、脏数据插入等等   0x01 靶场选择 靶场 DVWA Openrasp-testcases 技术栈 PHP/MYSQL JAVA/PHP/MYSQL 支持漏洞类型 https://github.com/di...

    2022年6月13日
    10900
  • 关于MySQL,这篇都没人赞,太没天理了!

    这是一篇关于MySQL数据库,redo log,LSN,崩溃恢复,在线热备的长文,耐心读完,如果没有收获,可以捶我。   研发的童鞋每次对MySQL库表做重大操作之前,例如: (1)修改表结构; (2)批量修改或者删除数据; 都会向DBA申请进行数据库的备份。 画外音:又或者说,不备份直接操作啦?   那DBA童鞋是怎么进行MySQL备份的呢?  ...

    2022年5月11日
    1900
  • 你的业务代码,是不是都写在了Activity里?

    互联网分层架构演进有两条核心原则: (1)让上游更高效的获取与处理数据(复用); (2)让下游能屏蔽数据的获取细节(封装); 数据从数据库/缓存层,到微服务层,到站点应用层,最终都汇聚到客户端。服务端的分层架构设计已经讲了很多,客户端的分层架构设计应该怎么玩呢,服务端的分层架构设计是否有能够借鉴的地方呢,今天和大家简单聊一聊。   先来看小诗一首:   《A...

    2022年5月11日
    5100
  • 究竟为什么,快速排序的时间复杂度是n*lg(n)? | 经典面试题

    最烦面试官问,“为什么XX算法的时间复杂度是OO”,今后,不再惧怕这类问题。   快速排序分为这么几步: 第一步,先做一次partition; partition使用第一个元素t=arr[low]为哨兵,把数组分成了两个半区: 左半区比t大 右半区比t小 第二步,左半区递归; 第三步,右半区递归;   伪代码为: void quick_sort(int[]a...

    2022年5月11日
    2700

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

联系我们

400-800-8888

在线咨询: QQ交谈

邮件:admin@example.com

工作时间:周一至周五,9:30-18:30,节假日休息

关注微信