中间件之redis高级特性Lua脚本
一、Lua脚本
1.认识Lua脚本
使用Lua执行Redis命令的好处:
- 一次发送多个命令,减少网络开销
- Redis会将整个脚本作为一个整体执行,不会被其他请求打断,保证原子性
- 对于复杂的组合命令,我们可以放在
文件/bean/内存
中,实现复用。
redis > eval lua-script key-num[key1 key2 ...][value1 value2 ...]
- eval代表执行lua语言
- lua-script:lua脚本内容
- key-num:参数中有多少个key,注意:Redis中key是从1开始的,如果没有key的参数,写0
- key1 key2…:key作为参数传递给脚本,需要和key-num的个数一致
- value1 value2…:作为参数传递给脚本,可填可不填
# 使用lua脚本写HelloWorld
redis > eval "return 'hello world'" 0
redis.call(command,key[param1,param2...])
- command:命令,包括set、get、del等等
- key:被操作的key
- param1、param2:key的参数
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 me 1000
OK
127.0.0.1:6379> get me
"1000"
127.0.0.1:6379>
2.简单使用
场景1:
传入key,将此key自增value个值,返回执行结果。
lua脚本:
if ARGV[1]==nil then
return 'false';
else
local quota = tonumber(redis.pcall('get', KEYS[1])) or 0;
if quota + ARGV[1] >0 then
redis.pcall('incrbyfloat',KEYS[1],tonumber(ARGV[1]));
return {'true'};
else
return {'false'};
end
end
使用redistemplate的api执行
@RequestMapping("/lua1")
public String lua1() {
DefaultRedisScript<Object> script = new DefaultRedisScript<>();
script.setResultType(Object.class);
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/demo2.lua")));
List<Object> keys = new ArrayList<>();
keys.add("luakey");
List<Float> argvList = new ArrayList<>();
argvList.add(1.1f);
Object[] argvArr = argvList.toArray();
Boolean execute = (Boolean) redisTemplate.execute(script, keys, argvArr);
Object luakey = redisTemplate.opsForValue().get("luakey");
return "result";
}
场景2
每个用户在X秒内只能访问Y次
思路:拿到IP后,设置IP为key,参数1传入过期时间,参数2为限定次数。如果没有超过限定次数,可以访问,返回true,如果超过了限定次数,返回fasle。超过限定时间,可以再次访问。
限定5s内只能访问30次。
lua脚本
local key = KEYS[1]
local expire = tonumber(ARGV[1])
local count = tonumber(ARGV[2])
local quota = tonumber(redis.call('get', key)) or 0
if quota==0 then
redis.call('incr',key)
redis.call('expire',key,expire)
return {'true'}
elseif quota>= count then
return {'false'}
else
redis.call('incr',key)
return {'true'}
end
redisTemplate
@RequestMapping("/lua")
public Object lua() throws UnknownHostException, InterruptedException {
for (int i = 0; i < 100; i++) {
DefaultRedisScript<Object> script = new DefaultRedisScript<>();
script.setResultType(Object.class);
script.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis/demo.lua")));
List<Object> keys = new ArrayList<>();
InetAddress localHost = InetAddress.getLocalHost();
String ip = new String(localHost.getAddress());
keys.add(ip);
List<Object> argvList = new ArrayList<>();
argvList.add(5);
argvList.add(30);
Object[] argvArr = argvList.toArray();
Boolean execute = (Boolean) redisTemplate.execute(script, keys, argvArr);
Object value = redisTemplate.opsForValue().get(ip);
System.out.println("当前总第" + (i + 1) + "次调用,5秒内调用次数:" + String.valueOf(value));
Thread.sleep(100);
}
return "success";
}
执行结果:
...
当前总第47次调用,5秒内调用次数:30
当前总第48次调用,5秒内调用次数:30
当前总第49次调用,5秒内调用次数:30
当前总第50次调用,5秒内调用次数:1
当前总第51次调用,5秒内调用次数:2
...
3.缓存lua脚本
在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给Redis服务,会产生比较大的网络开销。为了解决这个问题,Redis可以缓存Lua脚本并生成SHA1摘要码,后面可以直接通过摘要码来执行lua脚本。
首先在服务端缓存lua脚本生成一个摘要码,用script load
命令
script load "return 'Hello World'"
# "470877a599ac74fbfda41caa908de682c5fc7d4b"
第二个命令通过摘要码执行缓存
evalsha "470877a599ac74fbfda41caa908de682c5fc7d4b"
4.小结
如果我们有一些特殊的需求,可以用lua来实现,但是要注意那些耗时的操作,因为redis执行命令是单线程的,如果执行lua脚本时间过长,redis命令会阻塞在队列中。
可以在配置文件中配置脚本的执行时间,默认为5s,当阻塞时间超过5s,其他命令不会等待,直接返回“BUSY”错误。
lua-time-limit 5000
但是不能一直拒绝客户端的命令执行,使用script kill
命令来终止脚本的执行。
但是如果当前执行的lua脚本对Redis的数据进行了修改(SET,DEL)等,通过script kill命令是不能终止脚本运行的,只能通过shutdown nosave命令来停止redis服务。
二、内存回收
想要实现key过期,有几种思路
- 主动过期:每个设置过期时间的key都有一个定时器,到过期过期时间立即删除。优点是堆内存友好,缺点是会占用大量CPU资源来处理过期的数据,影响缓存的响应时间和吞吐量
- 惰性淘汰:当访问一个key的时候,判断key是否过期,过期则清除。优点是节省CPU资源,缺点是对内存不友好。如果出现大量过期key没有再次被访问,从而不会被清除,占用大量内存
1.定期删除
redis每个一定时间,会扫描一定数量的设置了过期时间的key,并清除其中已经过期的key。这么做可以在不同情况下使得CPU资源和内存资源达到一个平衡效果。
所以Redis使用了惰性删除和定期过期两种策略。
如果所有的key都没有设置过期时间,Redis内存满了怎么办?
2.淘汰策略
是指当内存达到使用的最大极限的时候,需要使用淘汰算法来清理一些数据,保证新的数据存入。
在redis.conf中可以配置内存淘汰策略。maxmemory-policy noeviction
策略 | 含义 |
---|---|
volatile-lru | 根据LRU算法删除设置了超时时间的key,直到腾出自购的内存空间位置。如果没有可删除的key,则回退到noeviction策略。 |
allkeys-lru | 根据LRU算法删除key,不管数据有没有设置超时时间属性 |
volatile-lfu | 根据LFU算法删除设置了超时时间的key |
allkeys-lfu | 根据LFU算法删除key |
volatile-random | 随机删除设置了过期时间的key |
allkeys-random | 随机删除所有key |
volatile-ttl | 根据键值对象的ttl属性,删除最近将要过期的数据。如果没有则回退到noeviction策略 |
noeviction | 默认策略,不会删除任何数据,拒绝所有写入操作并返回客户端错误信息,此时Redis只响应读操作。 |
建议使用volatile-lru,保证正常服务的情况下,优先删除最近最少使用的key。
LRU :Least Recently Used,最近最少使用
传统的LRU:通过链表+HashMap实现,设置链表长度,如果新增或者被访问移动元素到头节点。如果超过链表长度,末尾的元素就被删除。
LFU:Least Frequently Used,最不常使用
random:随机删除
三、持久化机制
Redis提供了两种持久化方案,一种是RDB快照,一种是AOF。
1.RDB
Rdb是Redis默认的持久化方案(如果开启了AOF,优先使用AOF)。当满足一定条件时,会将当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过加载dump.rdb文件恢复数据。
rdb文件位置和目录
# 文件路径
dir ./
# 文件名称
dbfilename dump.rdb
# 是否以LZF压缩rdb文件
rdbcompression yes
# 开启数据校验
rdbchecksum yes
触发
自动触发
- 按照配置规则自动触发
redis.conf SNAPSHOTTING,其中定义了触发把数据保存到磁盘的频率
以上只要满足任意一个就触发save 900 1 # 900秒内至少有一个key被修改 save 300 10 #300秒内至少有10个key被修改 save 60 10000 # 60秒内至少有10000个key被修改
- shutdown触发,保证服务器正常关闭
- flushall,清空redis数据的时候,会触发一次rdb
手动触发
如果我们需要重启服务或者迁移数据,这时候需要手动触发RDB快照保存。Redis中提供了两条命令:
- save
save在生成快照的时候会阻塞当前Redis队列,redis不能处理其他命令。如果内存中数据过多,不建议使用这个命令。 - bgsave
执行此命令,Redis会在后台异步进行快照操作,快照的同时还可以响应客户端请求。
我们可以通过备份RDB文件来恢复数据
RDB优势与劣势
优势
- RDB是一个很紧凑的文件,保存了redis在某个时间点上的数据集。很适合用于灾备恢复等场景。
- 生成RDB文件时,会由一个子线程来处理保存操作,之线程不需要进行任何io操作。
- RDB在恢复大数据集时速度比AOF快
劣势
- RDB没办法做到实时持久化。
- 每隔一定时间进行一次备份,如果redis意外宕机,会丢失最后一次快照后的数据。
2.AOF
Append Only File
Redis默认不开启AOF。AOF采用日志的形式来记录每个写操作,并追加 到文件中,开启后,执行更改Redis数据的命令时,会把命令写入到AOF文件中。
Redis重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。
配置
redis.conf
# 开关 默认关闭,开启改为yes
appendonly no
# 文件名,路径也是通过dir参数配置config get dir
appendfilename "appendonly.aof"
AOF持久化策略
持久化前缓存AOF数据机制配置。redis.conf中appendfsunc everysec
默认everysec,AOF持久化策略
- no 表示不执行fsync,由操作系统保证数据同步到磁盘,速度快,但不太安全
- always 表示每次写入都执行fsync,以保证数据同步到磁盘,效率低
- everysec 表示每秒执行一次fsync,可能会导致丢失1s的数据,通常选择everysec,兼顾安全性和效率。
(注意:这个策略有可能会丢失1s的数据)
。
重写AOF
当AOF文件特别大的时候,会占用服务器磁盘空间,AOF恢复时间也会变长。
通过命令bgrewriteaof
来重写AOF文件,这个命令会直接读取服务器现有的键值对,然后由一条命令代替之前记录这个键值对的多条命令(比如key1,先修改后删除再设值,三个操作合并为一个set命令)
,生成一个新的AOF文件取替换原来的文件
AOF优势与劣势
优点
- AOF持久化的方法提供了多种同步频率,即使使用默认的同步频率每秒同步1此,redis也就丢失1s的数据而已
缺点
2. 对于同一台redis服务器,AOF文件通常会比RDB文件体积更大。
3. 再高并发情况下,RDB比AOF具有更好的性能
3.小结
如果可以忍受一小段时间的数据丢失,毫无疑问使用RDB是最好的,定时生成快照非常便于进行数据备份,并且RDB的恢复速度也比AOF的恢复速度更快
否则就使用AOF重写。但是一般生产环境不建议单独使用某一种持久化机制,两种一起开启。这种情况下,redis重启时会优先载入AOF文件来恢复数据,因为通常情况下AOF文件保存的数据集比RDB问价能保存的数据集更完整。