基于 Redis 分布式锁

66 阅读2分钟

配置redis

gem 'uuid'
gem 'redis' # https://github.com/redis/redis-rb
redis_client = Redis.new({host:'192.168.0.84', port:6379, db:0})

实现一个简单的reids锁

# 加锁
redis_client.set('lock_name', 'abc',ex: 20,nx: true)
# 释放锁
redis_client.del('lock_name')

避免锁被其他人释放

如果超过20s线程a没有执行完,但是key已经过期,这时候key已经消失,此时线程b获取锁设置时间,这个时候线程a执行完毕,就会执行释放锁,把b刚加的锁给释放了。以需要增加判断是不是自己加的锁。

lock_uuid = UUID.new.generate
redis_client.set('lock_name', lock_uuid,ex: 20,nx: true)
# 释放锁,判断这个lock_uuid 是不是自己加的,如果是才释放
if redis_client.get('lock_name') == lock_uuid
  redis_client.del('lock_name')
end

这个时候又有极端问题发生了,如果线程a判断是自己的锁过后,在删除前,这个锁过期了,这个时候线程b又获取到锁了,结果线程a又给线程b把锁释放了。这时候使用lua脚本保持操作的原子性,不会被其他客户端打断。

# 释放锁
lua_script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
redis_client.eval(lua_script,:keys => ["lock_name"],:argv => [lock_uuid])
# 一个完整的事例
def redis_lock(key,uuid,ex = 20)
  # 一起设置确保原子性  
  redis_client.set(key, uuid,ex: ex,nx: true)
end

def redis_lock_del(key,uuid)
  # lua 脚本确保原子性  
  lua_script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end"
  redis_client.eval(lua_script,:keys => [key],:argv => [uuid])
end
def redis_lock_with_black(key,ex = 20,&block)
  lock_uuid = UUID.new.generate
  begin
    if redis_lock(key,lock_uuid,ex)
      yield block
    end
  rescue => e
    puts e
  ensure
    redis_lock_del(key,lock_uuid)
  end
end

超过预期的设置的时间,使用watchdog策略

这样看起来比较完美了,但是这样就结束了吗?如果在过期时间内代码没有执行完,锁就被其他线程获取了 在当前线程下,启动一个线程,用来做延期操作,只要这个锁没有被释放,就是需要延期的,java中设置为守护线程,主线程结束时,这个子线程也会结束。在ruby中可能需要使用信号或者自己启动线程来做监控。

在主从等reids服务上同步锁会有延迟,需要传输时间,无法确保一致性。

上面的操作都是在单个redis实例上操作的。如果redis是主从,就会从在延迟同步到问题。可以使用下面的工具。 RedLock 是一种分布式锁算法,旨在解决在分布式环境下实现强一致性分布式锁的问题。RedLock 使用多个 Redis 实例(各个实例无关联)进行加锁,并要求在大多数实例上成功获取锁才认为获取成功,以提高分布式锁的可靠性和容错性。

红锁 redlock