Redis高级应用
事务管理
redis的事务支持:
与其他NoSQL不同Redis是存在事务的,尽管没有传统数据库那么强大,但是还是非常有用,尤其是高并发的情况中,使用redis的事务可以保证数据一致性的同时,大幅度提高数据读写的响应速度
事务特点:
redis的事务是使用watch-multi-exec-discard-unwatch的命令组合,它可以提供两个重要保证:
1.事务是一个被隔离的操作,事务中的方法都会被redis进行序列化并按顺序执行,事务在执行的过程中不会被其他客户端的发出的命令所打断
2.事务是一个原子性操作,它要么全部执行、要么全部不执行
事务操作:
1.multi:开启事务(OK),之后的命令就会进入队列(QUEUED),而不是马上执行
2.exec:执行事务如果被监听的键没有被修改,则采用提交命令,否则就执行回滚命令
3.discard:回滚事务
4.watch key1 key2…:监听某些键,当被监听的键在提交事务前被修改,则事务会回滚(基于乐观锁机制)
5.unwatch key1 key2…:取消监听
注意:
redis不会判断数据类型,如果遇到命令语法格式正确而数据类型不符合的情况,不会事务回滚
Spring操作事务:需要保证是一个连接,所以不能直接使用模板对象
@Test
public void redisTransation(){
//事务必须是同一个连接,此时开启操作都是单挑连接
// redisTemplate.multi();
// redisTemplate.opsForValue().set("name","zhangsan");
// redisTemplate.opsForValue().set("age","19");
// String name = (String) redisTemplate.opsForValue().get("name");
// System.out.println(name);
// redisTemplate.exec();
//先保证同一个连接
redisTemplate.execute(new SessionCallback() {
@Override
public Object execute(RedisOperations operations) throws DataAccessException {
operations.multi();
operations.opsForValue().set("name","zhangsan");
operations.opsForValue().set("age","19");
String name = (String) redisTemplate.opsForValue().get("name");
System.out.println(name);
List list = operations.exec();
//返回结果
System.out.println(list);
return list;
}
});
}
流水线(了解)
为什么需要流水线操作?
在实际开发过程中,redis的性能瓶颈不在于自己计算能力,通常限制在于网络传输,为了减少网络传输所带来的redis性能闲置时间,因此就可以使用流水线,类似于批处理操作
注意:
流水线必须是同一个连接才能实现,所以在实际开发过程中,通常是多个人进行redis操作,这种情况下流程线是不适用的,如果一个人需要进行大量的redis操作,这个时候才需要考虑流水线
发布和订阅(了解)
发布与订阅的命令
1.subscribe chat
2.publish chat “message”
redis发布与订阅
redis提供了消息发布和订阅的功能,一方作为订阅者,订阅某个管道,另一方作为发布者,发布消息,订阅方就能及时接受到这个消息
redis的发布订阅和传统MQ的各自特点
redis特点:
轻量级,实时性,数据安全性低(消息发送出,有可能没被订阅者收到)
传统MQ特点:
专业级,能处理的消息量更大,实时性较低,数据安全性有保证
超时时间
概述:
1.给redis的key设置一个过期的时间,时间到之后该键值对就会被redis清理,节省内存
2.在实际开发过程中,因为内存的限制,并不可能保证redis和数据库的数据达到完全相同,因此只能尽可能保证redis中存储的都是热点数据(被频繁访问的数据)
3.对于这种情况,如何保证热点数据被存在redis中,通可以使用超时命令+内存管理(内存回收策略)两个方案:
在实际开发过程中,项目组通常都会要求开发者给所有的缓存数据添加超时时间
相关命令
1.persist key :
持久化key,即得永生(移除key的超时时间),所有的key默认就是永生的
2.[p(毫秒)]expire key seconds:
设置key的超时时间,单位是s,通常设置超时时间为5~10分钟
3.[p(毫秒)]ttl key :
查看key的剩余时间,单位是秒
-1表示永生
-2表示死亡
注意点:
redis在key过期后,不会立刻从内存中移除该键值对
因为如果需要从内存中移除过期的键对应的键值对,redis需要维护一个观察key是否过期的功能,这样肯定会降低redis的性能,这并不是开发者所希望看见的情况
以下三种情况下,redis会从内存中移除键值对:
1.当用户使用get命令获取一个已经超时的key时,redis会立刻从内存中清除该键值对
2.redis内部有一个定时器,默认是每秒清除一次过期的key
3.如果内存满了根据不同的淘汰策略,redis会清除某些键值对(这些key有可能没有过期,甚至是永生的)
Lua脚本语言
为什么需要Lua脚本?
具体的业务逻辑需要用java程序控制,因此redis2.6以后开始支持Lua脚本,Lua脚本端可以在redis实现复杂的业务逻辑
Lua脚本的另一个特点,可以保证整个脚本是原子性执行的,线程是安全的,能够保证并发数据一致性(基于redis的单线程模式)
类似于存储过程(但是存储过程是线程不安全的)
redis是单线程模型还是多线程模型?
什么是多线程模型?
eg:tomcat内部维护一个线程池,对于多个请求开启多条线程同时处理
什么是单线程模型?
eg:redis内部维护一个队列,对于多个客户端的多个请求进行排列后,然后单线程的依次处理每个请求
如何选择:
根据CPU的空闲时间判断
1.如果CPU的空闲时间比较多,则应该选择多线程;
2.如果CPU的空闲时间比较少,则应该选择单线程;
eg:
1.tomcat因为要处理很多复杂的业务并且与网络有关联,这些业务可能会导致线程阻塞,线程阻塞时,CPU为了不空闲,切换到其他线程处理请求
2.redis计算能力很高,所以CPU利用很高,在请求量很大的时候,几乎不会出现CPU空闲时间,这个时候如果采用多线程,redis还需要额外的线程切换消耗,得不偿失
Lua脚本语言语法
lua hello.lua
注解:
单行 –
多行–[[ ]]–
数据类型
type(“s”) = string:类型
“abc”=3:长度
“a”…“b”=“ab”:拼接字符串
####### 变量声明:
变量名=变量值: b = 10
####### 变量类型:
1.全局变量:global a = 20
2.局部变量:local b = 10
流程控制:
1.if & if-else:
lua if.lua 19 : 传参
age = tonumber(arg[1]) : 接受参数
if age > 19 then
print("adult") else print("child")
end
2.if嵌套(没有else if)
逻辑判断:
与或非:and/or/~
循环:
while(i <= 10)
do
print(i)
i = i + 1
end
for i=1,10(终止位置),1(步长)
do
print(i)
end
数组:
array={1,2,3,4,5}
for i=1,#array,1
do
print(array[i])
end
redis执行Lua脚本
命令格式:
eval lua-script key-num [key1 key2 key3 …] [value1 value2 value3 …]
解释:
eval:代表执行Lua语言的命令
lua-script:代表Lua语言脚本
key-num:整数,代表参数中有多少个key
[key1 key2 key3 …]:key作为参数传递给Lua语言
[value1 value2 value3 …]:作为参数传递给Lua,不是必填
eg:
eval “return ‘content’” 0
eval “redis.call(‘set’, KEYS[1], ARGV[1])” 1 hello world
spring执行Lua脚本
脚本字符串(开发中不会直接使用字符串)
public void luaScript(){
//准备手动序列化方式,解决模板和原生连接操作相差性行为
JdkSerializationRedisSerializer jdkSerializationRedisSerializer =
new JdkSerializationRedisSerializer();
//准备脚本
//1.直接执行脚本字符串
String luaStr = "return redis.call('set',KEYS[1],ARGV[1])";
//spring的redisTemplate并没有封装执行lua
//通过redisTemplate获得连接/2.0之前是jedis/直接使用即可
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//手动序列化
byte[] names = jdkSerializationRedisSerializer.serialize("name");
byte[] xuhengliangs = jdkSerializationRedisSerializer.serialize("xuhengliang");
//通过原始连接对象操作lua脚本
byte[] result = connection.eval(luaStr.getBytes(), ReturnType.VALUE, 1,names,xuhengliangs);
//用原始连接进行set不会序列化
System.out.println("执行脚本的结果: " + new String(result));
System.out.println(redisTemplate.opsForValue().get("name"));
}
缓存lua脚本
//缓存lua脚本
String luaStr = "return redis.call('set',KEYS[1],ARGV[1])";
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//开始缓存lua脚本,返回脚本的签名(id唯一标志)
String scriptLoad = connection.scriptLoad(luaStr.getBytes());
System.out.println("缓存脚本的签名:" + scriptLoad);//c686f316aaf1eb01d5a4de1b0b63cd233010e63d
//根据返回的签名,执行缓存脚本
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.evalSha("c686f316aaf1eb01d5a4de1b0b63cd233010e63d",ReturnType.VALUE,1,"money".getBytes(),"10000".getBytes());
执行lua文件(一般是lua文件和缓存一起使用)
@Test
public void luaScript() throws IOException {
//获得lua脚本文件
File file = new File(this.getClass().getResource("/static/lua/hello.lua").getPath());
//将文件读到内存中
// InputStream ips = new FileInputStream(file);
// ByteArrayOutputStream ops = new ByteArrayOutputStream();
//
// int len;
// byte[] buffer = new byte[1024];
// while((len = ips.read(buffer)) != -1){
// ops.write(buffer,0, len);
// }
// //得到脚本的byte数组
// byte[] bytes = ops.toByteArray();
//直接将文件转成字节数组,通过内存流ByteArrayOutputStream实现
byte[] bytes = FileUtils.readFileToByteArray(file);
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
//执行脚本 connection.eval(bytes,ReturnType.VALUE,0,"99".getBytes(),"xuhengliang".getBytes(),"lidiqiu".getBytes());
}
redis的持久化
持久化方式
快照RDB(默认行为):
快照(snapshotting)它是备份当前瞬间redis在内存中的数据结构,当重启redis之后,从快照中直接恢复数据到内存中即可
shutdown [save|nosave] : 默认关闭redis时会拍一次快照
相关命令:
1.save:
使用当前线程拍摄快照,快照的时候会拒绝客户端的所有写命令
2.bgsave:
开启一个新的线程拍摄快照,当前线程仍然会就收客户端写命令
相关配置:
1.save seconds times :
控制快照的频率
默认:save 900 1 / save 300 10 /save 60 10000
save “” : 配置关闭快照功能
2.stop-writes-on-bgsave-error yes :
bgsave一旦出错,前台写操作会被拒绝,错误反馈作用
3.dbfilename dump.rdb :
快照文件的文件名(默认即可)
4.dir ./ :
快照文件的存放路径 ./ 相对redis.conf
只追加文件AOF:
每次redis执行写命令时,会将该命令记录到一个文件中,当redis重启时,会从文件里面依次执行写命令进,而达到恢复数据的结果
相关配置:
1.appendonly no :
是否启动append only mode,默认关闭
2.appendfilename “appendonly.aof” : 文件名
appendfsnyc : 记录写操作的频率
always : 每次写都记录
everysec : 每秒钟记录一次(默认)
no : 不主动记录,让开发者通过命令被动的记录
3.避免aof文件过大的配置:
auto-aof-rewrite-percentage 100
一旦文件到达上一个文件的100就开启一个新的文件
auto-aof-rewrite-min-size 64mb
一旦文件到64mb就开启一个新的文件
aof-load-truncated yes :
aof文件恢复,如果最后一条命令出现问题是否忽略,默认是忽略
选择持久化模式
特点:
RDB:
恢复速度快,快照数据的安全性会比AOF低
AOF:
文件体积有可能很大,而且恢复速度有可能会很慢,
但数据的安全性比RDB高,一般最多丢失一秒以内的数据
如果希望数据的绝对安全,最好两个同时开启
当redis恢复数据时,会先恢复AOF文件再恢复快照
如果redis仅仅只是作为缓存服务器,两个都可以关闭
最大提示redis的性能,数据在数据库中有
一般情况下,推荐使用RDB就可以
RDB的性能损耗没有AOF那么大
redis内存回收策略
当内存存满的时候,redis该怎么办,取决于内存回收策略的配置
maxmemory-policy
用于配置redis的内存回收策略,当内存达到最大值时采取的内存处理方式
maxmemory-policy可选值:
noeviction (默认):不淘汰任何键值对,当内存已经满时,进入只读模式
volatile-lru:从所有设置了超时时间的key中,选出最近最少被使用
allkeys-lru:从所有key中,选出最近最少被使用
volatile-random:从所有设置了超时时间的key中随机选出某些key淘汰
allkeys-random:从所有key中随机选出某些key淘汰
volatile-ttl:从设置了超时时间的key,选出剩余时间最少的key
规律:
lru:最近最少使用
random:随机
ttl:剩余时间最少
volatile:从设置了超时时间的key中选择
allkeys:从所有的key中选择
注意:
LRU和TTl算法在redis中都不是精准计算,而是一个近似算法,抽样选择
redis默认有一个探测数量的配置maxmemory-samples默认为3,也就是抽样选择3个样本
随机抽取样本,在样本中选择符合淘汰策略key淘汰