有一类业务场景:
(1)超高吞吐量,每秒要处理海量请求;
(2)写多读少,大部分请求是对数据进行修改,少部分请求对数据进行读取;
这类业务,有什么实现技巧么?
接下来,一块儿听我从案例入手,娓娓道来。数据库
(1)司机地理位置信息会随时变化,可能
每几秒钟地理位置要修改一次
;
(2)用户打车的时候查看某个司机的地理位置,
查询地理位置的频率相对较低
;
void SetDriverInfo(long driver_id, DriverInfo info);
DriverInfo GetDriverInfo(long driver_id);
(2)返回value也定长,例如:司机实体序列化后的二进制串;
Map<driver_id, DriverInfo>
这个kv内存缓存是一个临界资源,对它的并发访问,有什么注意事项么?
void SetDriverInfo(long driver_id, DriverInfo info){缓存
WriteLock (m_lock);微信
Map<driver_id>= info;多线程
UnWriteLock(m_lock);架构
}并发
DriverInfo GetDriverInfo(long driver_id){app
DriverInfo t;运维
ReadLock(m_lock);高并发
t= Map<driver_id>;性能
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]<driver_id>= 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]); // 解锁行锁
}
这个方案使得锁冲突降到了最低,但锁资源大增,在数据量很是大的状况下,内存每每是装不下的。
画外音:数据量比较小的时候,能够一个元素一把锁,典型的是链接池,每一个链接用一把锁表示链接是否可用。
写多读少的业务,有一种优化方案:无锁缓存,将锁冲突下降到。
若是缓存不加锁,读写吞吐量能够达到极限,可是多线程对缓存中同一块定长数据进行写操做时,
有可能出现不一致的脏数据
。
画外音:做为缓存,容许
cache miss
,却不容许读脏数据。
(1)线程1对缓存进行操做,对
key
想要写入
value1
;
(2)线程2对缓存进行操做,对
key
想要写入
value2
;
(3)不加锁,线程1和线程2对同一个定长区域进行一个并发的写操做,
可能每一个线程写成功一半,致使出现脏数据产生
,最终的结果即不是
value1
也不是
value2
,而是一个乱七八糟的不符合预期的值
value-unexpected
;
并发写入的数据分别是
value1
和
value2
,读出的数据是
value-unexpected
,数据被篡改,这本质上是一个数据完整性的问题。
例如:运维如何保证,从中控机分发到上线机上的二进制没有被篡改?
又例如:即时通信系统中,如何保证接受方收到的消息,就是发送方发送的消息?
发送方除了发送消息自己,还要发送消息的签名
,接收方收到消息后要校验签名,以确保消息是完整的,未被篡改。
加入“签名”保证数据的完整性以后,读写流程须要如何升级?
加上签名以后,
不但缓存要写入定长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,能够从数据库中读取数据。
总结
(2)Map转Array的方式来最小化锁冲突,一条记录一个锁;
(4)经过签名的方式保证数据的完整性,实现无锁缓存;
若是你喜欢本文,大几率会喜欢这个架构训练营,欢迎一块儿来玩。