redis原理2
1:Lua 脚本
Lua/ˈluə/是一种轻量级脚本语言,它是用 C 语言编写的,跟数据的存储过程有点类似。
使用 Lua 脚本来执行 Redis 命令的好处:
- 一次发送多个命令,减少网络开销。
- Redis 会将整个脚本作为一个整体执行,不会被其他请求打断,保持原子性。
- 对于复杂的组合命令,我们可以放在文件中,可以实现程序之间的命令集复用。
1.1:在 Redis 中调用 Lua 脚本
使用 eval /ɪ'væl/ 方法,语法格式:
redis> eval lua-script key-num [key1 key2 key3 ....] [value1 value2 value3 ....]
- eval 代表执行 Lua 语言的命令。
- lua-script 代表 Lua 语言脚本内容。
- key-num 表示参数中有多少个 key,需要注意的是 Redis 中 key 是从 1 开始的,如果没有 key 的参数,那么写 0。
- [key1 key2 key3…]是 key 作为参数传递给 Lua 语言,也可以不填,但是需要和 key-num 的个数对应起来。
- [value1 value2 value3 ….]这些参数传递给 Lua 语言,它们是可填可不填的。
示例,返回一个字符串,0 个参数:
eval "return 'Hello World'" 0
1.2在 Lua 脚本中调用 Redis 命令
使用 redis.call(command, key [param1, param2…])进行操作。语法格式:
eval "redis.call('set',KEYS[1],ARGV[1])" 1 lua-key lua-value
- command 是命令,包括 set、get、del
- key 是被操作的键。
- param1,param2…代表给 key 的参数
注意跟 Java 不一样,定义只有形参,调用只有实参。Lua 是在调用时用 key 表示形参,argv 表示参数值(实参)。
1.2.1 设置键值对
在 Redis 中调用 Lua 脚本执行 Redis 命令
eval "return redis.call('set',KEYS[1],ARGV[1])" 1 fuyu 9527
get fuyu
以上命令等价于 set fuyu 9527
在 redis-cli 中直接写 Lua 脚本不够方便,也不能实现编辑和复用,通常我们会把脚本放在文件里面,然后执行这个文件。
1.2.2 通过Lua脚本,对IP进行限流
redis.properties
redis.host:10.19.206.98
redis.port:9400
redis.password:
public class JedisUtil {
private JedisPool jedisPool;
private JedisUtil(){
String host = ResourceBundle.getBundle("redis").getString("redis.host");
int port = Integer.parseInt(ResourceBundle.getBundle("redis").getString("redis.port"));
String password = ResourceBundle.getBundle("redis").getString("redis.password");
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
if(StringUtils.isBlank(password)){
jedisPool = new JedisPool(jedisPoolConfig,host,port,10000);
}else{
jedisPool = new JedisPool(jedisPoolConfig,host,port,10000,password);
}
}
public Jedis getJedis(){
return jedisPool.getResource();
}
/**
* 获取指定key的值,如果key不存在返回null,如果该Key存储的不是字符串,会抛出一个错误
*
* @param key
* @return
*/
public String get(String key){
Jedis jedis = getJedis();
String value = null;
value = jedis.get(key);
return value;
}
@Configurable
public class TestLua {
private static Jedis jedis;
public static void main(String[] args) {
jedis = JedisUtil.getJedisUtil().getJedis();
jedis.eval("return redis.call('set',KEYS[1],ARGV[1])",1,"test:lua:key","fuyu");
jedis.get("test:lua:key");
for (int i = 0; i < 10; i++) {
Limit();
}
}
/**
* 10秒内限制访问5次
*/
public static void Limit(){
// 只在第一次对key设置过期时间
String lua = "local num = redis.call('incr', KEYS[1])\n" +
"if tonumber(num) == 1 then\n" +
"\tredis.call('expire', KEYS[1], ARGV[1])\n" +
"\treturn 1\n" +
"elseif tonumber(num) > tonumber(ARGV[2]) then\n" +
"\treturn 0\n" +
"else \n" +
"\treturn 1\n" +
"end\n";
Object result = jedis.evalsha(jedis.scriptLoad(lua), Arrays.asList("localhost"), Arrays.asList("10", "5"));
System.out.println("************"+result);
}
}
1.2.3缓存 Lua 脚本
为什么要缓存
在脚本比较长的情况下,如果每次调用脚本都需要把整个脚本传给 Redis 服务端,会产生比较大的网络开销。为了解决这个问题,Redis 提供了 EVALSHA 命令,允许开发者通过脚本内容的 SHA1 摘要来执行脚本。
如何缓存Redis
在执行script load命令时会计算脚本的SHA1摘要并记录在脚本缓存中,执行EVALSHA命令时Redis会根据提供的摘要从脚本缓存中查找对应的脚本内容
2.内存回收
Reids 所有的数据都是存储在内存中的,在某些情况下需要对占用的内存空间进行回收。内存回收主要分为两类,一类是key过期,一类是内存使用达到上限(max_memory)触发内存淘汰。
2.1 过期策略
2.1.1 定期过期
每隔0.1s检查一次,从设置过期时间的key中,随机测试20个有过期时间的key,然后删除已过期的key,如果有25%的key被删除,则重复执行整个流程。
2.1.2 惰性过期(被动淘汰)
只有当访问一个key时,才会判断该 key 是否已过期,过期则清除。
该策略可以最大化地节省 CPU 资源,却对内存非常不友好。极端情况可能出现大量的过期key没有再次被访问,从而不会被清除,占用大量内存。
Redis中同时使用了惰性过期和定期过期两种过期策略。
2.2 淘汰策略
如果都不过期,Redis内存满了,达到最大内存极限时,需要使用淘汰算法来决定清理掉哪些数据,以保证新数据的存入。
建议使用volatile-lru:在过期集合的键中,尝试回收使用最少的键
2.2.1问题:
如何找出热度最低的数据?
Redis 中所有对象结构都有一个 lru 字段, 且使用了 unsigned 的低 24 位,这个字段用来记录对象的热度。对象被创建时会记录 lru 值。在被访问的时候也会更新 lru 的值。但是不是获取系统当前的时间戳,而是设置为全局变量 server.lruclock 的值。
server.lruclock 的值怎么来的?
Redis中有个定时处理的函数serverCron ,默认每100毫秒调用函数updateCachedTime更新一次全局变量的server.lruclock的值,它记录的是当前 unix时间戳
3.持久化机制
Redis 速度快,很大一部分原因是因为它所有的数据都存储在内存中。如果断电或者宕机,都会导致内存中的数据丢失。为了实现重启后数据不丢失,Redis 提供了两种持久化的方案,一种是 RDB 快照(Redis DataBase),一种是 AOF(Append Only File)
3.1 RDB
RDB是Redis默认的持久化方案。当满足一定条件的时候,会把当前内存中的数据写入磁盘,生成一个快照文件dump.rdb。Redis重启会通过加载dump.rdb文件恢复数据。
什么时候写入rdb 文件?
3.1.1 RDB 触发
a、自动触发
save 900 1 #900秒内至少有一个key被修改(包括添加)
save 300 10 #400秒内至少有10个key被修改
save 60 10000 #60秒内至少有10000个key被修改
只要满足任意一个都会触发
b、手动触发
如果我们需要重启服务或者迁移数据,这个时候就需要手动触 RDB 快照保存。
bgsave 命令
执行bgsave时,Redis调用fork操作创建一个子进程,子进程负责将快照写入硬盘,完成后自动结束,而父进程则继续处理命令请求。
它不会记录fork之后后续的命令。阻塞只发生在fork 阶段,一般时间很短。
用 lastsave 命令可以查看最近一次成功生成快照的时间。
3.1.2RDB文件的优势和劣势
一、优势
1.RDB是一个非常紧凑的文件,它保存了redis在某个时间点上的数据集。这种文件非常适合用于进行备份和灾难恢复。
2.生成RDB文件的时候,redis主进程会fork()一个子进程来处理所有保存工作,主进程不需要进行任何磁盘IO操作。
3.RDB在恢复大数据集时的速度比 AOF 的恢复速度要快。
二、劣势
1、RDB方式数据没办法做到实时持久化/秒级持久化。因为bgsave每次运行都要执行 fork 操作创建子进程,频繁执行成本过高。
2、在一定间隔时间做一次备份,所以如果redis意外down 掉的话,就会丢失最后一次快照之后的所有修改(数据有丢失)。
如果数据相对来说比较重要,希望将损失降到最小,则可以使用AOF方式进行持久化。
3.2 AOF
Redis 默认不开启。AOF 采用日志的形式来记录每个写操作,并追加到文件中。
开启后,执行更改 Redis 数据的命令时,就会把命令写入到 AOF 文件中。
Redis 重启时会根据日志文件的内容把写指令从前到后执行一次以完成数据的恢复工作。
3.2.1 AOF 配置
配置文件 redis.conf
# 开关
appendonly no
# 文件名
appendfilename "appendonly.aof"
参数 说明 appendonly appendfilename "appendonly.aof" 路径也是通过 dir 参数配置 config get d| | |
参数 | 说明 |
---|---|
appendonly | Redis默认只开启RDB持久化,开启AOF需要修改为 yes |
appendfilename | 路径也是通过 dir 参数配置 config get dir |
问题:由于AOF持久化是Redis不断将写命令记录到AOF 文件中,随着 Redis 不断的进文件越来越大,怎么办?
Redis 新增了重写机制,当 AOF 文件的大小超过所设定的阈值时,Redis 就会启动 AOF 文件的内容压缩,只保留可以恢复数据的最小指令
AOF 文件重写并不是对原文件进行重新整理,而是直接读取服务器现有的键值对,
然后用一条命令去代替之前记录这个键值对的多条命令,生成一个新的文件后去替换原来的 AOF 文件。
# 重写触发机制
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64
参数 | 说明 |
---|---|
auto-aof-rewrite-percentage | 默认值为 100。aof 自动重写配置,当目前 aof 文件大小超过上一次重写的 aof 文件大小的百分之多少进行重写,即当 aof 文件增长到一定大小的时候,Redis 能够调用 bgrewriteao对日志文件进行重写。当前 AOF 文件大小是上次日志重写得到 AOF 文件大小的二倍(设置为 100)时,自动启动新的日志重写过程。 |
auto-aof-rewrite-min-size | 默认 64M。设置允许重写的最小 aof 文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重 |
3.2.2 AOF 优势与劣势
优点:
1、AOF 持久化的方法提供了多种的同步频率,即使使用默认的同步频率每秒同步一次,Redis最多也就丢失1秒的数据而已。
缺点:
1、对于具有相同数据的Redis,AOF文件通常会比RDF文件体积更大(RDB存的是数据快照)。
2、虽然AOF提供了多种同步的频率,默认情况下,每秒同步一次的频率也具有较高的性能。
在高并发的情况下,RDB 比 AOF 具好更好的性能保证。
3.2.3 两种方案比较
那么对于 AOF 和 RDB 两种持久化方式,我们应该如何选择呢?
如果可以忍受一小段时间内数据的丢失,毫无疑问使用RDB是最好的,定时生成RDB快照非常便于进行数据库备份,并且RDB恢复数据集的速度也要比AOF恢复的速度要快。
否则就使用AOF重写。但是一般情况下建议不要单独使用某一种持久化机制,而是应该两种一起用,在这种情况下,当 redis 重启的时候会优先载入AOF文件来恢复原始的数据,因为在通常情况下AOF文件保存的数据集要比 RDB 文件保存的数据集要完整。