四时宝库

程序员的知识宝库

Redis性能优化:使用Lua脚本编程,重写锁

Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过内嵌对 Lua 环境的支持, Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。本文将介绍如何使用Lua重写锁,和重写之前与重写之后的性能对比。

前期准备

本文使用的是Python Redis客户端,为了防止客户端并未为Redis2.6提供直接载入或者执行Lua脚本的功能,所以我们需要花费一点时间来创建一个脚本载入程序。

将脚本载入Redis,需要用到一个名为SCRIPT LOAD的命令,这个命令接受一个字符串格式的Lua脚本为参数,它会把脚本存储起来等待之后使用,然后返回被存储脚本的SHA1校验和。之后,用户只要调用EVALSHA命令,并输入脚本的SHA1校验和以及脚本所需的全部参数,就可以调用之前存储的脚本。

将脚本载入Redis的script_load函数

1、将SCRIPT LOAD命令返回的已载入脚本的SHA1校验和存储到一个列表里面,以便之后在call()函数内部对其进行修改。

2、在调用已载入脚本的时候,用户需要将Redis连接、脚本要处理的键以及脚本的其他参数传递给脚本。

3、程序只会在SHA1校验和未被缓存的情况下尝试载入脚本。

4、使用以缓存的SHA1校验和执行命令。

5、如果错误与脚本缺失无关,那么重新抛出异常。

6、当程序接收到脚本错误时,或者程序需要强制执行脚本时,它会使用EVAL命令直接执行给定的脚本。EVAL命令在执行完脚本之后,会自动把脚本缓存起来,而缓存产生的SHA1校验和跟使用EVALSHA命令缓存脚本产生的SHA1校验和是完全相同的。

7、返回一个函数,这个函数在被调用的时候会自动载入并执行脚本。

除了调用SCRIPT LOAD命令和EVALSHA命令之外,script_load()函数还会捕捉一个异常,当函数缓存了某个脚本的SHA1校验和,但是服务器却并没有存储这个SHA1校验和对应的脚本时,异常就会被抛出。

在服务器重启之后,或者用户执行了SCRIPT FLUSH命令,清空脚本缓存之后,又或者程序在不同的时间给函数提供了指向不同Redis服务器的连接时,这个异常都会出现。

当函数检测到脚本缺失的时候,它就会使用EVAL命令直接执行脚本,而EVAL命令,除了会执行脚本之外,还会将被执行的脚本缓存到Redis服务器里面。

除此之外,script_load()函数还允许用户通过force_eval参数来直接执行脚本,当我们需要在事务或者流水线里面执行脚本的时候,这个功能就会非常有用。

为什么要重写锁

第一个原因

可以将CAS操作变为一个原子操作。这样做的主要目的是为了让Redis的集群服务器可以拒绝那些尝试在指定的分片上面,对不可用的键进行读取或者写入的脚本。

第二个原因

减少网络通信次数。在处理Redis存储的数据时,程序可能需要一些数据,但这些数据没办法再最开始的调用中取得。其中的一个例子就是,从Redis获取一些散列值,然后使用这些值去访问存储在关系型数据库里面的信息,最后再把这些信息写入Redis里面。

基于以上这两个原因,我们需要使用Lua脚本重写锁实现。

重写锁实现

加锁操作首先生成一个ID,然后使用SETNX命令对键进行了有条件的设置操作,并在设置操作执行成功的时候,为键设置了过期时间。尽管加锁操作在概念上并不复杂,但程序还是需要处理各种失败和重试情况。

原版代码如下:

重写之前加锁实现源代码

1、128位随机标识符。

2、确保传给EXPIRE的都是整数。

3、获取锁并设置过期时间。

4、检查过期时间,并在有需要时对其进行更新。

使用Lua重写之后的代码:

重写之后加锁实现源代码

1、执行实际的锁获取操作,通过检查确保Lua调用已经执行成功。

2、检测锁是否已经存在。(提醒,Lua表格的索引是从1开始的。)

3、使用给定的过期时间以及标识符去设置键。

除了将之前的SETNX命令和EXPIRE命令替换成SETEX命令,从而确保客户端获取的锁总是具有过期时间之外,Lua脚本实现的加锁操作跟原来的加锁操作之间并无明显的不同。

接下来让我们乘胜前进,继续使用Lua脚本重写锁的释放操作。

锁释放操作首先要做的就是使用WATCH去监视代表锁的键,检查该键是否仍然存储着加锁时设置的标识符。如果是的话,程序就解除锁;如果不是的话,程序就说指定的锁已经丢失。

使用Lua重写的release_lock函数

1、调用负责释放锁的Lua函数。

2、检查锁是否匹配。

3、删除锁并确保脚本总是返回真值。

跟加锁操作不同,Lua版本的锁释放操作比原版更为简洁,因为程序无需再执行典型的WATCH/MULTI/EXEC步骤。

虽然减少代码量是一件非常好的事情,但是如果Lua版本的锁实现不能带来实际的性能提升,那么它的作用将是非常有限的。

为了测试原版锁实现和Lua锁实现之间的性能差异,我们给这两种锁实现的代码增加了一些指令,并通过测试代码分别执行1个、2个、5个和10个并行的进程,让这些进程反复不断的对锁执行获取操作和释放操作,然后记录两个版本的锁实现在十秒内执行锁获取操作的次数以及成功取得锁的次数。结果如图所示:

原版锁实现和Lua版本的锁实现在10秒内的性能对比

通过观察表中右边那一栏可以看到,在测试循环里面,Lua版本的锁实现在获取锁和释放锁方面的表现,要明显优于原版锁实现:在使用单个客户端的情况下,Lua锁的性能要高40%多;在使用两个客户端的情况下,Lua锁的性能要高87%;而在使用五个或者十个客户端的情况下,Lua锁的性能要高一倍以上。

通过对比中间栏和右边栏,我们还可以看到,由于Lua版本的锁实现,减少了加锁时所需的通信往返次数,所以Lua版本的锁实现在尝试获取锁时的速度比原版的锁要快得多。

除了性能变得更好之外,Lua版本的加锁操作和锁释放操作的代码也明显的变得更容易理解了,这使得我们可以很容易的验证这些代码的正确性。

总结

使用Lua脚本可以极大地提高性能,并对需要执行的操作进行大幅的简化。大家可以多尝试一下。

本文作者长期致力于互联网技术研究,擅长互联网相关知识包括高并发、大数据、架构、前后端语言、框架、算法、常见面试题等,欢迎关注。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接