数据库和缓存的数据一致性
1.概述
为了保证并发访问的正确性,Redis
提供了两种方法:加锁和原子操作。
1.1 加锁
概念:在读取数据前,客户端需要先获取锁,否则无法进行操作;当一个客户端获取锁后,就会一直持有这把锁,直到客户端完成数据更新,才释放这把锁。
问题:
- 加锁操作多,降低系统的并发访问性能;
Redis
客户端加锁时,需要用到分布式锁,而分布式锁实现复杂,需要用额外的存储系统来提供加解锁操作。
并发访问中对什么进行控制?
指对多个客户端访问操作同一份数据的过程进行控制,以保证任何一个客户端发送的操作在Redis
实例上执行时具有互斥性。并发访问控制主要是数据修改操作。修改数据时,基本流程为两步:
- 客户端先把数据读取到本地,在本地进行修改;
- 客户端修改完数据后,再写回
Redis
。
我们把这个流程叫做 “读取-修改-写回” 操作(Read-Modify-Write
,简称为 RMW
操作)。访问同一份数据的RMW
操作代码,叫做临界区代码。
例子:
加锁前:
出现这个现象的原因是,临界区代码中的客户端 读取数据、更新数据、再写回数据 涉及了三个操作,而这三个操作在执行时并不具有互斥性,多个客户端基于相同的初始值进行修改,而不是基于前一个客户端修改后的值再修改。
加锁后:
1.2 Redis原子操作方法
- 把多个操作放到
Redis
中实现成一个操作,即单命令操作; - 把多个操作写到一个
Lua
脚本中,以原子性方式执行单个Lua
脚本。
1.2.1 单命令操作
Redis
是使用单线程来串行处理客户端的请求操作命令的。当执行某个命令操作时,其他命令是无法执行的,这相当于命令操作是互斥执行的。在实际应用中,数据修改需要包含多个操作,至少包含读数据、数据增减、写数据。Redis
提供了incr/decr
命令,将三个操作转变成一个原子操作了。所以,若我们执行的RMW
操作是对数据增减的话,incr/decr
可以帮助我们进行并发控制。
Lua脚本
Redis
会把整个Lua
脚本作为一个整体执行,执行的过程中,不会被其他命令打断,从而保证了Lua
脚本中操作的原子性。如果我们有多个操作要执行,但是又无法用incr/decr
这种命令来实现,就可以把要执行的操作写到Lua
脚本中,使用Redis
的eval
命令来执行脚本,这些操作就具有了互斥性。
总结
在并发访问时,并发的RMW
操作会导致数据错误,所以需要进行并发控制。所谓并发控制,就是要保证临界区代码的互斥执行。Redis
提供了两种原子操作的方法来实现并发控制,分别是单命令操作和Lua
脚本,因为原子操作本身不会对太多的资源限制访问,可以维持较高的系统并发性能。但是,单命令原子操作的使用范围较小,并不是所有的RMW
操作都能转变成单命令的原子操作(例如incr/decr
命令只能在读取数据后做原子增减),当我们需要对读取的数据做更多的判断,或者是我们对数据的修改不是简单的增减时,单命令操作就不适用了。而Redis
的Lua
脚本可以包含多个操作,这些操作都会以原子性的方式执行,绕开了单命令的限制。不过,如果把很多操作都放在Lua
脚本中原子执行,会导致Redis
执行脚本的时间增加,同样也会降低Redis
的并发性能。所以,建议:在编写Lua
脚本时,避免把不需要做并发控制的操作写入脚本中。当然,加锁也能实现临界区代码的互斥执行,只是如果有多个客户端加锁时,就需要分布式锁的支持了。