面试题答案
一键面试实现思路
- 利用SETNX命令解决锁竞争:
- Redis的SETNX(SET if Not eXists)命令具有原子性。当使用SETNX key value命令时,如果指定的key不存在,该命令会设置key的值为value并返回1,表示获取锁成功;如果key已存在,则不做任何操作并返回0,表示获取锁失败。通过这种方式可以实现多个客户端对锁的竞争。例如:
SETNX lock_key unique_value
- 这里
unique_value
通常是一个唯一标识符,如UUID,用于标识获取锁的客户端,方便后续解锁操作。
- 设置锁的过期时间:
- 可以在获取锁后,使用EXPIRE命令设置锁的过期时间,以防止某个客户端获取锁后出现异常而导致锁一直无法释放。例如:
EXPIRE lock_key expiration_time
- 或者在Redis 2.6.12及以上版本,可以在使用SETNX获取锁时,直接使用SET命令的扩展参数,在设置key - value的同时设置过期时间,例如:
SET lock_key unique_value EX expiration_time NX
- 这样就可以在一个原子操作中完成锁的获取和过期时间的设置,避免了获取锁后设置过期时间之间出现异常导致过期时间未设置的情况。
- 锁的续租问题:
- 可以通过一个守护线程(例如在Java中可以使用
ScheduledExecutorService
)来定时检查锁是否快要过期,如果快要过期并且当前客户端仍然持有锁,则使用EXPIRE
命令延长锁的过期时间。例如,假设锁的过期时间为30秒,守护线程可以在20秒左右检查并续租。 - 续租的代码示例(以Java和Jedis为例):
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(1); executorService.scheduleAtFixedRate(() -> { Jedis jedis = new Jedis("localhost", 6379); String lockValue = jedis.get("lock_key"); if (lockValue.equals(uniqueValue)) { jedis.expire("lock_key", expiration_time); } jedis.close(); }, 0, 20, TimeUnit.SECONDS);
- 可以通过一个守护线程(例如在Java中可以使用
保证字典操作原子性确保锁机制正确性
- 使用Redis的单线程特性:
- Redis是单线程模型,这意味着Redis处理命令是一个接一个顺序执行的。所以对于单个Redis实例,只要是使用Redis的命令进行操作,就天然保证了原子性。例如前面提到的SETNX、SET以及EXPIRE等命令在执行时都是原子的,不会出现并发操作导致数据不一致的情况。
- Lua脚本:
- 在一些复杂场景下,可能需要多个命令组合来完成操作,例如获取锁并设置过期时间的原子操作在低版本Redis中需要多个命令,此时可以使用Lua脚本来保证原子性。因为Redis执行Lua脚本是原子的,将多个相关命令写到一个Lua脚本中,然后使用
EVAL
命令执行脚本,就可以保证这一组操作的原子性。例如:
-- 获取锁并设置过期时间的Lua脚本 local key = KEYS[1] local value = ARGV[1] local expiration_time = ARGV[2] local result = redis.call('SETNX', key, value) if result == 1 then redis.call('EXPIRE', key, expiration_time) end return result
- 然后在客户端使用
EVAL
命令执行这个脚本:
EVAL "脚本内容" 1 lock_key unique_value expiration_time
- 这里
1
表示后面跟着1个key(即lock_key
),unique_value
和expiration_time
是传递给Lua脚本的参数。通过这种方式可以在高并发场景下保证复杂的字典操作(锁操作)的原子性,确保锁机制的正确性。
- 在一些复杂场景下,可能需要多个命令组合来完成操作,例如获取锁并设置过期时间的原子操作在低版本Redis中需要多个命令,此时可以使用Lua脚本来保证原子性。因为Redis执行Lua脚本是原子的,将多个相关命令写到一个Lua脚本中,然后使用