Redis 高级应用 -- 事务和分布式锁

本贴最后更新于 1831 天前,其中的信息可能已经东海扬尘

简单用法

Redis 使用 MULTI、EXEC、DISCARD 和 WATCH 等命令实现事务。下面是 Redis 事务的用法,使用 MULTI 命令开始后,Redis 会判断输入的命令是否是 MULTI、EXEC、DISCARD 和 WATCH 中的一个,如果是,则执行命令,否则会将命令保存在队列中,最后执行 EXEC 命令提交事务。Redis 执行事务期间,服务器不会去执行其他命令,等事务中所有命令执行完毕才会处理其他请求。

127.0.0.1:6379> MULTI  # 事务开始
OK
127.0.0.1:6379> SET name "11231" #命令入队
QUEUED
127.0.0.1:6379> GET name  #命令入队
QUEUED
127.0.0.1:6379> EXEC  #事务执行
1) OK
2) "11231"

Redis 提供 DISCARD 命令取消事务:

127.0.0.1:6379> SET name "123"
OK
127.0.0.1:6379> GET name
"123"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DEL name
QUEUED
127.0.0.1:6379> DISCARD   # 取消事务
OK
127.0.0.1:6379> GET name
"123"

如果输入的命令语法错误,会直接报错.但是 Redis 无法判断输入的指令是否存在逻辑错误。例如下面的例子,Redis 在事务执行前可以判断出来"XPUSH"是不合法的命令,但是无法判断"name"是字符串类型,而不是列表型,只有执行后才报错。

127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> XPUSH 123 123
(error) ERR unknown command 'XPUSH'
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors.

127.0.0.1:6379> del name
(integer) 1
127.0.0.1:6379> SET name 123  # name是字符串类型
OK
127.0.0.1:6379> GET name
"123"
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> LPUSH name 123 321  # 将name作为列表型保存
QUEUED
127.0.0.1:6379> EXEC
1) (error) WRONGTYPE Operation against a key holding the wrong kind of value

WATCH 命令

WATCH 命令是一个乐观锁,它可以在 EXEC 命令执行之前,监视任意数量的 key,并在 EXEC 命令执行时,检查被监视的键是否至少有一个已经修改,如果是的话,服务器拒绝执行事务,并返回空恢复。

127.0.0.1:6379> WATCH name  # 开始监视name键
OK
127.0.0.1:6379> SET name 321 # 这个命令修改了name的值,这里模拟的是其他客户端修改name得值
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET haha 321  # 这里可以执行任意指令,因为watch监视的键,和事务中操作的键没有关联。
QUEUED
127.0.0.1:6379> EXEC # EXEC命令执行前,name的值已经被修改,所以返回nil
(nil)
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET name 321
QUEUED
127.0.0.1:6379> EXEC # 第二次执行事务,WATCH的监视已经消失,虽然前面name已经被修改,这里还可以操作name
1) OK
127.0.0.1:6379> GET name
"321"

还可以使用 UNWATCH 命令取消所有键的监视。

回滚

Redis 的事务和关系型数据库事务最大的区别是没有事务失败后的回滚操作。如下例子,如果事务执行失败,在失败命令之前的命令都会执行,并且无法回滚,需要手动回滚。

127.0.0.1:6379> KEYS *
(empty list or set)
127.0.0.1:6379> SET name gavin # name是字符串类型
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> SET age 1 
QUEUED
127.0.0.1:6379> LPUSH name 123 321 # name是字符串型,这里当成数组使用,会报错
QUEUED
127.0.0.1:6379> EXEC
1) OK  
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
127.0.0.1:6379> KEYS *  #事务执行后,虽然报错,但是age已经被添加进去,没有回滚
1) "age"
2) "name"
127.0.0.1:6379>

分布式锁

分布式使用 SETNX(set if not exists)设置公共锁。SETNX 命令,可以判断 key 是否有值,有值返回设置失败,无值则返回设置成功。

SETNX key value
  • 对于返回设置成功的,拥有控制权,进行下一步的业务操作
  • 对于返回设置失败的,不具有控制权,则排队或者等待

死锁

Redis 分布式锁中,使用 SETNX 命令进行简单的上锁,如果上锁的机器因为一些原因掉线,将会一直占用锁,造成死锁,因此需要一些特殊处理。参考资料中 Redisson 给出了一个很好的解决办法:
首先使用正常的方式加锁,再为 myLock 设置 30 秒的生存时间,避免发生死锁。

SETNX myLock 客户端id #设置客户端id,这样客户端在下次进入时判断哪个客户端占用锁,实现可重入锁
pexpire myLock 30000
.....     # 执行业务操作
del myLock   # 执行完毕删除锁

然后客户端通过一个线程每 10s 就去判断客户端是否释放锁,如果客户端还持有锁,然后就延长锁的生存时间。这样就保证了客户端存活时一直占有锁,掉线时则自动过期,让别的客户端取占有锁。当然真实业务中不可能持有锁 30 秒,一般使用如下规则:

  • 例如:持有锁的操作最长执行时间 127ms,最短执行时间 7ms
  • 测试百万次最长执行时间对应命令的最大耗时,测试百万次网络延迟平均耗时
  • 锁时间设定推荐:最大耗时 * 120% + 平均网络延迟* 110%
  • 如果业务最大耗时 << 网络平均延迟,通常为 2 个数量级,取其中单个耗时较长即可

参考资料

Redis 分布式锁

  • Redis

    Redis 是一个开源的使用 ANSI C 语言编写、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。从 2010 年 3 月 15 日起,Redis 的开发工作由 VMware 主持。从 2013 年 5 月开始,Redis 的开发由 Pivotal 赞助。

    286 引用 • 248 回帖 • 44 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...