当前位置: 首页 > 科技观察

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

时间:2023-03-16 15:12:45 科技观察

有一类业务场景:超高吞吐量,每秒处理大量请求;写多读少,大部分请求是修改数据,少量请求是读取数据;这类业务的实现技巧是什么?下面,听我从案例入手,说一说。快狗打车,比如:司机的位置信息会随时变化,可能每隔几秒就需要修改一次位置;用户在打车时查看司机位置时,查询位置的频率较低;这里我们使用到两个接口:大量修改驱动信息:voidSetDriverInfo(longdriver_id,DriverInfoinfo);比较少量的司机信息查询:DriverInfoGetDriverInfo(longdriver_id);您通常如何实施此类业务?具体来说,底层实现往往是一个Mapmemorycache:querykey是固定长度的,比如:driverID;返回值也是固定长度的,如:驱动实体序列化后的二进制字符串;也就是这样一个kv缓存结构:Map这个kv内存缓存是一个并发访问关键资源有什么注意事项吗?访问关键资源需要注意加读写锁,实现互斥。下面是加锁和写入的伪代码:voidSetDriverInfo(longdriver_id,DriverInfoinfo){WriteLock(m_lock);Map=info;UnWriteLock(m_lock);}画外音:假设info已经序列化。下面是加锁和读取的伪代码:DriverInfoGetDriverInfo(longdriver_id){DriverInfo;ReadLock(m_lock);t=Map;UnReadLock(m_lock);return;}吞吐量大的时候,上面可能存在什么过程问题?假设快狗有10万个驱动同时在线,每个驱动每5秒更新一次经纬度状态,那么每秒有20万个并发写操作。假设快狗每天有1,000,000个打车订单,平均每秒约有300个订单,对应的并发查询量为每秒约1,000个并发读取操作。在这样的吞吐量下(每秒20万次写入,1k次读取),锁m_lock会成为潜在的瓶颈,导致Map访问效率极低。有什么潜在的优化方法吗?锁冲突严重的原因是整个Map共享一个锁,锁粒度太粗。画外音:可以认为是数据库的“库级锁”。是否可以水平拆分来减少锁冲突?答案是肯定的。画外音:类似于数据库中的分库,可以将一个库锁变成多个库锁,提高并发性,减少锁冲突。我们可以将1个Map横向分割成N个Map:voidSetDriverInfo(longdriver_id,DriverInfoinfo){i=driver_id%N;//横向分割成N个部分,N个Map,N个锁WriteLock(m_lock[i]);//锁定第i个锁Map[i]=info;//操作第i个MapUnWriteLock(m_lock[i]);//解锁第i个锁}这个优化能提升性能吗?一个Map变成了ThereareNMap,每个Map的并发度变成了1/N;同时每个Map的数据量变为1/N;所以理论上,锁冲突会呈指数下降,性能会下降提升。是否可以进一步细化锁粒度,每个元素一个锁?答案也是肯定的。画外音:可以认为是数据库的“库级锁”,被优化为“行级锁”。我们把driver_id设置为增量生成,假设内存比较大。这时可以把Map优化成Array,把锁的粒度细化到最细,每个驱动信息一个锁:voidSetDriverInfo(longdriver_id,DriverInfoinfo){index=driver_id;WriteLock(m_lock[index]);//超大内存,一记录一锁,行锁Array[index]=info;//driver_id是Array的下标UnWriteLock(m_lock[index]);//UnlockRowlock}这种方案最大限度的减少了锁冲突,但是锁资源大大增加了。当数据量很大时,内存往往装不下。画外音:当数据量比较小的时候,可以对每个元素使用一把锁,典型的是一个连接池,每个连接使用一把锁来表示连接是否可用。还是没有办法进一步减少锁冲突,提高并发?对于写多读少的业务,有一个优化方案:无锁缓存,将锁冲突降到最低。无锁缓存可能有什么问题?如果缓存不加锁,读写吞吐量可以达到极限,但是当多个线程写入缓存中同一个定长数据时,可能会出现不一致的脏数据。该方案牺牲了一致性以提高性能。读取时得到错误的数据是不可接受的。画外音:作为缓存,允许缓存未命中,但不允许读到脏数据。脏数据是如何产生的?在没有加锁的情况下,当多个线程并发写入时,可能会出现以下情况:线程1操作缓存,想将value1写入key;线程2对缓存进行操作,想写入keyEntervalue2;在没有加锁的情况下,线程1和线程2对同一个定长区域进行并发写操作,每个线程都可能写入一半成功,从而产生脏数据。最后的结果既不是value1也不是value2,而是乱七八糟的value-unexpectedvalue-unexpected;如何解决上述问题?本质上,这是一个数据完整性问题。并发写入的数据分别是value1和value2,读取的数据是value-unexpected,数据被篡改了,本质上是数据完整性问题。您通常如何确保数据的完整性?比如:运维如何保证从中控机分发到线上机的binary没有被篡改?MD5。又如:在即时通讯系统中,如何保证接收方收到的消息就是发送方发送的消息?发送方不仅发送消息本身,还发送消息的签名。接收方在收到消息后,需要对签名进行校验,以确保消息的完整性和未被篡改。“签名”是确保数据完整性的常用方案。加上“签名”保证数据的完整性后,如何升级读写流程呢?添加签名后,不仅定长值本身要写入缓存,定长签名(如16bitCRC校验)也要写入缓存:(1)线程1对缓存进行操作,想将value1写入密钥,并写入签名v1-sign;(2)线程2对缓存进行操作,想将value2写入key,写入签名v2-sign;(3)如果没有锁,线程1和线程2对同一个定长区域进行并发写操作,每个线程都可能写入一半成功,导致脏数据。最后的结果既不是value1也不是value2,而是一个Messyvalue-unexpectedvalue,不符合预期,但是签名必须是v1-sign或者v2-sign;画外音:16bit/32bit的写法可以保证原子性。(4)读取数据时,不仅要取出值,还要验证签名,就好像消息的接收者收到了消息一样。如果发现签名不一致,缓存会返回NULL,即缓存未命中;当然对应driver的geographyLocation,除了内存缓存之前,肯定需要一个定时器,定时将数据放到缓存中,写入数据库。如果缓存未命中,可以从数据库中读取数据。巧了,秒?总结当业务遇到:超高并发;更多的写入和更少的读取;fixed-lengthvalue;,可以采用以下方法提高吞吐量:(1)水平拆分,减少锁冲突;想法:单个数据库变量多个库。(2)MaptoArray方式,尽量减少锁冲突,一记录一锁;思路:库锁变成行锁。(3)无锁,最大化并发;想法:行锁变为无锁,完整性和性能之间的折衷。(4)通过签名的方式保证数据的完整性,实现无锁缓存;思路:写的时候写签名,读的时候验证签名。【本文为专栏作者《58神剑》原创稿件,转载请联系原作者】点此阅读更多该作者好文