面试题答案
一键面试- 读写锁设计思路
- 读锁:
- 读操作可以并发执行,因为读操作不会修改数据,所以多个读操作之间不会产生数据一致性问题。
- 可以使用Redis的SETNX(SET if Not eXists)命令来实现简单的读锁。例如,当一个读操作开始时,尝试使用SETNX命令设置一个特定的读锁键,如
read_lock:{业务标识}
,如果设置成功(返回1),表示获取到读锁,可以进行读操作;如果设置失败(返回0),表示其他读操作或写操作已经持有锁,需要等待。
- 写锁:
- 写操作必须是独占的,以保证数据的一致性。
- 同样可以使用SETNX命令来实现写锁,例如设置
write_lock:{业务标识}
。只有获取到写锁的操作才能进行写操作。写操作完成后,需要使用DEL命令删除写锁键来释放锁。
- 读锁:
- 避免锁竞争和性能优化
- 锁的粒度控制:
- 尽量缩小锁的粒度。对于复杂事务逻辑中涉及的多个键值对操作,如果可以将其拆分为多个小的操作,并且这些小操作之间不存在数据依赖,可以对每个小操作分别加锁。例如,如果事务涉及对
key1
、key2
、key3
的操作,且key1
与key2
、key3
无依赖,可以先对key1
加锁操作,完成后释放锁,再对key2
、key3
加锁操作。这样可以减少锁的持有时间,降低锁竞争。
- 尽量缩小锁的粒度。对于复杂事务逻辑中涉及的多个键值对操作,如果可以将其拆分为多个小的操作,并且这些小操作之间不存在数据依赖,可以对每个小操作分别加锁。例如,如果事务涉及对
- 读写锁的优化:
- 读锁优化:为了减少读锁等待时间,可以使用Redis的发布/订阅功能。当写操作完成并释放写锁时,通过发布一个消息通知所有等待读锁的客户端,这样等待读锁的客户端可以快速获取读锁进行读操作,而不需要一直轮询SETNX命令。
- 写锁优化:可以引入一个排队机制,例如使用Redis的List数据结构。当写操作获取写锁失败时,将该写操作的相关信息(如操作内容、客户端标识等)放入一个特定的List中,如
write_queue:{业务标识}
。持有写锁的客户端完成写操作后,从List中取出下一个写操作任务,这样可以有序地处理写操作,避免写操作的无序竞争。
- 锁的粒度控制:
- 原子性读写操作实现
- MULTI - EXEC事务:
- Redis提供了MULTI和EXEC命令来实现事务。对于需要原子性读写的多个键值对操作,可以在获取到写锁后,使用MULTI命令开启事务,然后依次执行多个读写操作命令,最后使用EXEC命令提交事务。例如:
- MULTI - EXEC事务:
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET key1 value1
QUEUED
127.0.0.1:6379> GET key2
QUEUED
127.0.0.1:6379> EXEC
1) OK
2) "value2"
- Lua脚本:
- 对于更复杂的原子性操作,可以使用Lua脚本。将多个Redis命令封装在一个Lua脚本中,然后使用EVAL命令在Redis服务器端执行。Lua脚本在执行过程中是原子性的,不会被其他命令打断。例如,下面的Lua脚本实现了对两个键值对的原子性更新:
-- 接收两个参数,第一个是键1,第二个是键2
local key1 = ARGV[1]
local key2 = ARGV[2]
-- 获取键1的值
local value1 = redis.call('GET', key1)
-- 更新键2的值为键1的值
redis.call('SET', key2, value1)
-- 更新键1的值为新值
redis.call('SET', key1, 'new_value')
在客户端可以使用如下命令执行该Lua脚本:
127.0.0.1:6379> EVAL "local key1 = ARGV[1] local key2 = ARGV[2] local value1 = redis.call('GET', key1) redis.call('SET', key2, value1) redis.call('SET', key1, 'new_value')" 0 key1 key2
OK
- 代码示例(以Python为例)
import redis
import time
def get_read_lock(redis_client, lock_key):
while True:
if redis_client.setnx(lock_key, 1):
return True
time.sleep(0.1)
def release_read_lock(redis_client, lock_key):
redis_client.delete(lock_key)
def get_write_lock(redis_client, lock_key):
while True:
if redis_client.setnx(lock_key, 1):
return True
time.sleep(0.1)
def release_write_lock(redis_client, lock_key):
redis_client.delete(lock_key)
def atomic_operation(redis_client, key1, key2):
if get_write_lock(redis_client, 'write_lock:example'):
try:
pipe = redis_client.pipeline()
pipe.multi()
pipe.set(key1, 'new_value1')
pipe.set(key2, 'new_value2')
pipe.execute()
finally:
release_write_lock(redis_client, 'write_lock:example')
if __name__ == '__main__':
r = redis.Redis(host='localhost', port=6379, db = 0)
atomic_operation(r, 'key1', 'key2')
以上通过设计合适的读写锁机制、优化锁操作以及利用Redis的事务和Lua脚本功能,在保证数据完整性和一致性的同时,尽量减少锁竞争带来的性能损耗。