超高并发的无锁缓存
最后更新于:2022-04-02 04:13:05
[TOC]
## 业务场景
有一类**写多读少**的业务场景:大部分请求是对数据进行修改,少部分请求对数据进行读取。
如:滴滴,出租车司机的定位(写多),查询(读少)
## 普通方案
具体到底层的实现,往往是一个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;
}
```
在并发量很大的时候(每秒20w写,1k读),锁m_lock会成为潜在瓶颈
## 水平切分+锁粒度优化
上文中之所以锁冲突严重都公用一把锁,锁的粒度太粗(“库级别锁”)把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)了,所以理论上,锁冲突会成平方指数降低,分库之后,仍然是库锁
## 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]); // 解锁行锁
}
```
锁冲突降到了最低,但锁资源大增
## 变成无锁缓存
```
void AddCountByType(long type /*, int count*/){
//不加锁
Array[type]++; // 计数++
//Array[type] += count; // 计数增加count
}
```
可以达到最高的并发,但是多线程对缓存中同一块定长数据进行操作时,有可能出现不一致的数据块,这个方案为了提高性能,牺牲了一致性。在读取计数时,获取到了错误的数据,是不能接受的(作为缓存,允许cache miss,却不允许读脏数据)
### 脏数据是如何产生的
![UTOOLS1577611201657.png](http://yanxuan.nosdn.127.net/5afa2d26635801ab0313b746f21116c0.png)
可能每个线程写成功一半,导致出现脏数据产生,最终的结果即不是value1也不
是value2,而是一个乱七八糟的不符合预期的值value-unexpected
即时通讯系统中,通过验证签名,保证接受方收到的消息,就是发送方发送的消息.
![UTOOLS1577611404281.png](http://yanxuan.nosdn.127.net/58465675b0895f07e77b159331f3945f.png)
加上签名之后,不但缓存要写入定长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中的任意一个
4. 数据读取的时候,不但要取出value,还要像消息接收方收到消息一样,校验一下签名,如果发现签名不一致,缓存则返回NULL,即cache miss。
对应到司机地理位置,与URL访问计数的case,除了内存缓存之前,肯定需要timer对缓存中的数据定期落盘,写入数据库,如果cache miss,可以从**数据库中读取数据**
';