pika作为类redis的存储系统,为了弥补在性能上的不足,在整个系统中大量使用多线程的结构,涉及到多线程编程,势必需要为线程加锁来保证数据访问的一致性和有效性。其中主要用到了四种锁
- 互斥锁
- 读写锁
- 行锁
互斥锁和挑拣锁比较常见,不过多描述,这里主要说一下另外两种锁的应用。
读写锁
应用场景
应用挂起指令,在挂起指令的执行中,会添加写锁,以确保,此时没有其他指令执行。其中挂起指令有:
- trysync
- bgsave
- flushall
- readonly
作用和意义
保证当前服务器在执行挂起指令时,只有一个线程处在running状态,而其他指令,则可以并发同时在不同线程中执行。
行锁
行锁,用于对一个key加锁,保证同一时间只有一个线程对一个key进行操作。
作用和意义:
pika中存取的数据都是类key,value数据,不同key所对应的数据完全独立,所以只需要对key加锁可以保证数据在并发访问时的一致性,行锁相对来说,锁定粒度小,也可以保证数据访问的高效性。
应用场景
在pika系统中,对于数据库的操作都需要添加行锁,主要在应用于两个地方,在系统上层指令过程中和在数据引擎层面。在数据引擎层面添加行锁就可以保证对于数据库访问的一致性和正确性。在pika系统中,指令执行的地方有两处:
- worker 用于执行从客户端接收到的指令
- bgworker 用于执行从master同步过来
的指令
在worker和bgworker需要添加行锁的地方,都是针对于写指令(会改变数据状态,如SET,HSET)。pika的主动同步是依靠binlog来完成,而binlog中则记录改变数据状态的指令,来保证master和slave的数据库状态一致。一条写指令的执行,主要有两个部分:
更改数据库状态
将指令添加到binlog中
为了保证写指令可以正确添加到binlog中,需要对整个指令的执行过程加锁。如下图
如果执行的是读指令,数据库状态不会改变,那么则不需要在pika层面对key添加行锁,只需要在nemo层面添加行锁就可以保证数据访问的正确性。
具体实现
在pika系统中,一把行锁就可以维护所有key。在行锁的实现上是将一个key与一把互斥锁相绑定,并将其放入哈希表中维护,来保证每次访问key的线程只有一个,但是不可能也不需要为每一个key保留一把互斥锁,只需要当有多条线程访问同一个key时才需要锁,在所有线程都访问结束之后,就可以销毁这个绑定key的互斥锁,释放资源。
为实现每个key的锁按需存在,在这里实现了RefMutex,对普通Mutex进行了一层封装,其中维护了访问所绑定key的线程个数ref。当一个线程访问key时,先在行锁维护的哈希表中查找,如果已经有了,则直接ref+1,并加锁;如果没有则创建RefMutex,放入哈希表中。当一个线程访问结束,则ref减1,如果ref为0,则销毁这把互斥锁。