知识卡片
单点写+读验证:让无锁并发读写在"读多写少"场景下既快又对
内容
QConf客户端在操作共享内存时是多进程并行读取的典型场景,读操作远多于写操作,为了尽可能提高读效率,团队选择了无锁操作共享内存这条路,但无锁化天然要面对两个正确性风险:多个写入者并发写可能互相破坏数据、读取者可能读到一份还没完全写完的中间态数据。QConf用两个配套措施分别应对这两个风险。第一个措施是单点写:所有需要写共享内存的场景(用户进程通过消息队列请求新Key、ZooKeeper的Watcher通知触发更新、定时重新注册Watcher导致的更新、Agent重启或网络异常后重新拉取数据),全部被收敛到唯一一个线程(Main线程)来执行,其他任何需要触发写操作的地方都只能通过中间数据结构(WaitingWriting队列)和这个唯一写线程通信、排队等待被处理——这样一来,”多个写入者并发写破坏数据”这个风险从设计上被直接消除了,代价是牺牲了一部分写入的并发效率,但因为整体场景本身读多写少,这个代价是可以接受的。第二个措施是读验证:由于写操作本身没有加锁保护读者,无锁的读写方式理论上确实存在读到未完全写入数据的风险,QConf团队的判断是——在绝对读多写少的环境下,这种”读到脏数据”的情况发生概率本身就很低,与其为了消灭这个低概率场景而引入昂贵的锁机制拖慢所有读操作,不如坦然接受它可能发生,转而在读取时做验证:写入时给数据带上一个MD5校验值,读取时用这个预存的MD5值来验证这次读到的数据是否完整正确,一旦验证失败就知道这次读到了不完整的数据,可以重试。这套组合拳给出了一条并发正确性设计的通用思路:面对”高并发读、低频写”的场景,与其无差别地用锁保护所有操作、牺牲读性能去换取绝对的写安全,不如把”写”这一侧的复杂度用单点化收敛掉、把”读”这一侧可能出现的低概率异常用轻量级的事后验证去兜底,这样读操作的性能几乎不受影响,同时整体正确性依然有保障。