Ⅰ 如何在 Redis 实现 Lua 脚本事务
在 1998-2003 年间,如果你想运行一个正规的数据库驱动的网站/服务,但又没有足够的资金购买微软或 Oracle 的数据库,你可以选择 MySQL 或 Postgres 。很多人都选择了 MySQL,因为它速度较快——主要是因为 MyISAM 存储引擎没有提供事务功能以此来换取性能,但速度确实很快。另一些人转向 Postgres,因为虽然在相同硬件上其性能明显低于 MySQL,但 Postgres 不会丢失数据(说实话,MySQL 数据丢失的情况非常少见,但丢了可不是闹着玩的)。
就这样凑合着过了很久;MySQL 将其默认的存储引擎从 MyISAM 过渡到了 InnoDB (其实很早
Ⅱ 使用redis实现的分布式锁原理是什么
一、写在前面
现在面试,一般都会聊聊分布式系统这块的东西。通常面试官都会从服务框架(Spring Cloud、Dubbo)聊起,一路聊到分布式事务、分布式锁、ZooKeeper等知识。
所以咱们这篇文章就来聊聊分布式锁这块知识,具体的来看看Redis分布式锁的实现原理。
说实话,如果在公司里落地生产环境用分布式锁的时候,一定是会用开源类库的,比如Redis分布式锁,一般就是用Redisson框架就好了,非常的简便易用。
大家如果有兴趣,可以去看看Redisson的官网,看看如何在项目中引入Redisson的依赖,然后基于Redis实现分布式锁的加锁与释放锁。
下面给大家看一段简单的使用代码片段,先直观的感受一下:
大家看到了吧,那个myLock的hash数据结构中的那个客户端ID,就对应着加锁的次数
(5)释放锁机制
如果执行lock.unlock(),就可以释放分布式锁,此时的业务逻辑也是非常简单的。
其实说白了,就是每次都对myLock数据结构中的那个加锁次数减1。
如果发现加锁次数是0了,说明这个客户端已经不再持有锁了,此时就会用:
“del myLock”命令,从redis里删除这个key。
然后呢,另外的客户端2就可以尝试完成加锁了。
这就是所谓的分布式锁的开源Redisson框架的实现机制。
一般我们在生产系统中,可以用Redisson框架提供的这个类库来基于redis进行分布式锁的加锁与释放锁。
(6)上述Redis分布式锁的缺点
其实上面那种方案最大的问题,就是如果你对某个redis master实例,写入了myLock这种锁key的value,此时会异步复制给对应的master slave实例。
但是这个过程中一旦发生redis master宕机,主备切换,redis slave变为了redis master。
接着就会导致,客户端2来尝试加锁的时候,在新的redis master上完成了加锁,而客户端1也以为自己成功加了锁。
此时就会导致多个客户端对一个分布式锁完成了加锁。
这时系统在业务语义上一定会出现问题,导致各种脏数据的产生。
所以这个就是redis cluster,或者是redis master-slave架构的主从异步复制导致的redis分布式锁的最大缺陷:在redis master实例宕机的时候,可能导致多个客户端同时完成加锁。
Ⅲ redis有脚本语言吗
有,lua脚本语言
Redis脚本
使用脚本的好处:
减少网络开销。可以将多个请求通过脚本的形式一次发送,减少网络时延
原子操作。redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。因此在编写脚本的过程中无需担心会出现竞态条件,无需使用事务。
复用。客户端发送的脚步会永久存在redis中,这样,其他客户端可以复用这一脚本而不需要使用代码完成相同的逻辑。
--eval,告诉redis-cli读取并运行后面的lua脚本
path/to/redis.lua,是lua脚本的位置
KEYS[1] KEYS[2],是要操作的键,可以指定多个,在lua脚本中通过KEYS[1], KEYS[2]获取
ARGV[1] ARGV[2],参数,在lua脚本中通过ARGV[1], ARGV[2]获取。
- redis.call('set', 'foo', 'bar')local value=redis.call('get', 'foo') --value的值为bar
EVAL "lua-script" [key ...] [arg ...]
通过key和arg这两类参数向脚本传递数据,它们的值在脚本中分别使用KEYS和ARGV两个表类型的全局变量访问。
注意: EVAL命令依据参数key-number来将其后面的所有参数分别存入脚本中KEYS和ARGV两个table类型的全局变量。当脚本不需要任何参数时,也不能省略这个参数(设为0)
redis>EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 foo bar
OK
redis>GET foo"bar"EVALSHA命令
在脚本比较长的情况下,如果每次调用脚本都需要将整个脚本传给Redis会占用较多的带宽。为了解决这个问题,Redis提供了EVALSHA命令,允许开发者通过脚本内容的SHA1摘要来执行脚本,该命令的用法和EVAL一样,只不过是将脚本内容替换成脚本内容的SHA1摘要。
Redis在执行EVAL命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容,如果找到了则执行脚本,否则会返回错误:"NOSCRIPT No matching script. Please use EVAL."
在程序中使用EVALSHA命令的一般流程如下。
虽然这一流程略显麻烦,但值得庆幸的是很多编程语言的Redis客户端都会代替开发者完成这一流程。执行EVAL命令时,先尝试执行EVALSHA命令,如果失败了才会执行EVAL命令。
先计算脚本的SHA1摘要,并使用EVALSHA命令执行脚本。
获得返回值,如果返回“NOSCRIPT”错误则使用EVAL命令重新执行脚本。
SCRIPTLOAD "lua-script"
将脚本加入缓存,但不执行. 返回:脚本的SHA1摘要
SCRIPT EXISTS lua-script-sha1
判断脚本是否已被缓存
SCRIPT FLUSH
清空脚本缓存 redis将脚本的SHA1摘要加入到脚本缓存后会永久保留,不会删除,但可以手动使用SCRIPT FLUSH命令情况脚本缓存。
SCRIPT KILL
强制终止当前脚本的执行。 但是,如果当前执行的脚步对redis的数据进行了写操作,则SCRIPT KILL命令不会终止脚本的运行,以防止脚本只执行了一部分。脚本中的所有命令,要么都执行,要么都不执行。
调用Lua脚本的语法:
$ redis-cli --eval path/to/redis.lua KEYS[1] KEYS[2] , ARGV[1] ARGV[2] ...
注意:
KEYS和ARGV中间的 ',' 两边的空格,不能省略。
redis支持大部分Lua标准库
库名
说明
Base 提供一些基础函数
String 提供用于字符串操作的函数
Table 提供用于表操作的函数
Math 提供数学计算函数
Debug 提供用于调试的函数
在脚本中调用redis命令
在脚本中可以使用redis.call函数调用Redis命令
redis.call函数的返回值就是Redis命令的执行结果
Redis命令的返回值有5种类型,redis.call函数会将这5种类型的回复转换成对应的Lua的数据类型,具体的对应规则如下(空结果比较特殊,其对应Lua的false)
redis返回值类型和Lua数据类型转换规则
redis返回值类型
Lua数据类型
整数回复 数字类型
字符串回复 字符串类型
多行字符串回复 table类型(数组形式)
状态回复 table类型(只有一个ok字段存储状态信息)
错误回复 table类型(只有一个err字段存储错误信息)
redis还提供了redis.pcall函数,功能与redis.call相同,唯一的区别是当命令执行出错时,redis.pcall会记录错误并继续执行,而redis.call会直接返回错误,不会继续执行。
在脚本中可以使用return语句将值返回给客户端,如果没有执行return语句则默认返回nil
Lua数据类型和redis返回值类型转换规则
Lua数据类型
redis返回值类型
数字类型 整数回复(Lua的数字类型会被自动转换成整数)
字符串类型 字符串回复
table类型(数组形式) 多行字符串回复
table类型(只有一个ok字段存储状态信息) 状态回复
table类型(只有一个err字段存储错误信息) 错误回复
脚本相关命令
Redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令都必须等待脚本执行完成后才能执行。为了防止某个脚本执行时间过长导致Redis无法提供服务(比如陷入死循环),Redis提供了lua-time-limit参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“BUSY”错误
Ⅳ redis lua脚本有什么用
主要用途是: (1)描述界面:WOW和剑网三的界面都是用LUA写的; (2)沟通引擎:游戏图形引擎提供了一些接口库,可以在LUA中调用; (3)服务器端:有些游戏,例如剑网三,在服务器端也会大量使用LUA。
Ⅳ redis执行lua脚本的时候会不会删除过期键
生命周期为一天,现在执行lua脚本,逻辑是判断a是否存在,存在则往其中添加其它的元素
Ⅵ hash类型的redis怎样实现联合查询
Redis不仅仅是一个简单的key-value内存数据库,Redis官网对自身的定义是“数据结构服务器”。通过用心设计各种数据结构类型的数据存储,可以实现部分的数据查询功能。因为在Redis的设计中,key是一切,对于Redis是可见的,而value对于Redis来说就是一个字节数组,Redis并不知道你的value中存储的是什么,所以要想实现比如
‘select * from users where user.location="shanghai"’
这样的查询,在Redis是没办法通过value进行比较得出结果的。但是可以通过不同的数据结构类型来做到这一点。比如如下的数据定义
users:1 {name:Jack,age:28,location:shanghai}
users:2 {name:Frank,age:30,location:beijing}
users:location:shanghai [1]
其中users:1 users:2 分别定义了两个用户信息,通过Redis中的hash数据结构,而users:location:shanghai 记录了所有上海的用户id,通过集合数据结构实现。这样通过两次简单的Redis命令调用就可以实现我们上面的查询。
Jedis jedis = jedisPool.getResource();
Set<String> shanghaiIDs = jedis.smembers("users:location:shanghai");
//遍历该set
//...
//通过hgetall获取对应的user信息
jedis.hgetAll("users:" + shanghaiIDs[0]);
通过诸如以上的设计,可以实现简单的条件查询。但是这样的问题也很多,首先需要多维护一个ID索引的集合,其次对于一些复杂查询无能为力(当然也不能期望Redis实现像关系数据库那样的查询,Redis不是干这的)。
但是Redis2.6集成了Lua脚本,可以通过eval命令,直接在RedisServer环境中执行Lua脚本,并且可以在Lua脚本中调用Redis命令。其实,就是说可以让你用Lua这种脚本语言,对Redis中存储的key value进行操作,这个意义就大了,甚至可以将你们系统所需的各种业务写成一个个lua脚本,提前加载进入Redis,然后对于请求的响应,只需要调用一个个lua脚本就行。当然这样说有点夸张,但是意思就是这样的。
比如,现在我们要实现一个‘所有age大于28岁的user’这样一个查询,那么通过以下的Lua脚本就可以实现
public static final String SCRIPT =
"local resultKeys={};"
+ "for k,v in ipairs(KEYS) do "
+ " local tmp = redis.call('hget', v, 'age');"
+ " if tmp > ARGV[1] then "
+ " table.insert(resultKeys,v);"
+ " end;"
+ "end;"
+ "return resultKeys;";
执行脚本代码
Jedis jedis = jedisPool.getResource();
jedis.auth(auth);
List<String> keys = Arrays.asList(allUserKeys);
List<String> args = new ArrayList<>();
args.add("28");
List<String> resultKeys = (List<String>)jedis.evalsha(funcKey, keys, args);
return resultKeys;
注意,以上的代码中使用的是evalsha命令,该命令参数的不是直接Lua脚本字符串,而是提前已经加载到Redis中的函数的一个SHA索引,通过以下的代码将系统中所有需要执行的函数提前加载到Redis中,我们的系统维护一个函数哈希表,后续需要实现什么功能,就从函数表中获取对应功能的SHA索引,通过evalsha调用就行。
String shaFuncKey = jedis.scriptLoad(SCRIPT);//加载脚本,获取sha索引
funcTable.put(funcName_age, shaFuncKey);//添加到函数表中
通过以上的方法,便可以使较为复杂的查询放到Redis中去执行,提高效率。
Ⅶ redis加载lua脚本,怎么获取数据
需要用lua写redis的操作函数,先连接redis数据库,然后执行命令,来获取数据。
Ⅷ 为什么在 Redis 实现 Lua 脚本事务
从很多方面来看,Redis 很像当初采用 InnoDB 前的 MySQL。而 Redis 采用了一种很合理的方式来保证数据完整性(复制,AOF 等),并且从 Redis2.6 开始引入的 Lua 脚本在功能与易用性方面为 Redis 的成长提供了很大助力。
相对来说,Lua 脚本与其他数据库中的存储过程很相似,但脚本的执行有些许不同。在本文中最重要的一点就是一旦将脚本写入数据库,它会一直执行直到以下任一种情况出现:
1. 完成所有工作,所有写操作处理完成后脚本会自动退出。
2. 脚本运行时出错并中途退出,所有以前执行的写操作都已发生,但不会再有其他写操作。
3. Redis 通过 SHUTDOWN NOSAVE 关闭时(不保存)。
4. 你附加了调试器来“使”脚本完成 #1 与 #2 (或其他手段来保证不会丢失数据)。
对于使用数据库开发软件的人,我想你也认同只有情景 #1 是最理想的。情景 #2,#3,#4 都会导致数据异常(#2 与
#4)和/或数据丢失(#3 和 #4)。如果你很重视数据,你应该尽可能地阻止数据异常与丢失。这不是哲学,而是工作(This is not
philosophy, this is doing your job)。但很遗憾目前的 Redis 也帮不了你多少。所以我决定改变这种情况。
Ⅸ Redis中嵌套结构的替代方案
基本上有两种策略:
可以序列化复杂对象并将其存储为字符串。我们建议使用json或msgpack作为序列化格式。这很容易从大多数客户端语言进行操作。如果需要服务器端访问,则服务器端的Lua脚本可以轻松地对此类对象进行编码/解码,因为Redis是使用针对Lua的msgpack和json支持进行编译的。
可以将对象拆分为不同的键。除了存储user:id和该ID的复杂数据结构之外,还可以存储几个键,如user:id,user:id:address_list,user:id:document_lists等…如果需要原子性,则对MULTI / EXEC块可用于保证数据一致性并汇总往返次数。
Redis不是面向文档的数据库。如果确实有很多复杂的文档,那么MongoDB,ArangoDB,CouchDB,Couchbase等解决方案可能会更好地提供服务