Redis入门级指南
redis是一个开源的KV结构的一个高性能内存数据库,他支持多种数据数据类型存储,有字符串String 、散列Hash、列表List、集合Set、有序集合Zset,是一个NoSQL( 非关系型数据库 )数据库
Redis的应用场景
内存数据库(登录信息、购物车信息、用户浏览记录等)
缓存服务器(商品数据、广告数据等等)。(最多使用)
解决分布式集群架构中的session分离问题(session共享)。
任务队列。(秒杀、抢购、12306等等)
支持发布订阅的消息模式
应用排行榜。
网站访问统计。
数据过期处理(可以精确到毫秒)
Redis没有官方的Windows版本,我们都是在Linux上安装运行
安装教程:https://www.cnblogs.com/msi-chen/p/10244202.html 或者 https://www.cnblogs.com/msi-chen/p/10719083.html
最近要学习很多需要Linux环境的技术,我专门下了CentOS,用作练习用:
安装教程,用的CentOS7 : https://www.cnblogs.com/tianyamoon/p/9418958.html
相关设置太多了,我提供几个常用的 :
开端口,和CentOS6不一样了 : firewall-cmd --permanent --zone=public --add-port=6379/tcp
Redis客户端
一个是Redis自带的 redis-cli 不做讲解,可以去翻上面的博客有介绍
Java客户端——Jedis
运用的最多的一个类型 https://github.com/xetorthio/jedis
简单的随便来个列子吧:分别为获取单列连接 和 从连接池中获取连接
Redis数据类型
String类型(字符类型)
set key value get key
mset k v k v k v mget k k k :同时设置或获取多个值
getset key value : 取值再赋值
incr key / incyby key num :递增 / 递增指定整数(value为整数才可用),比如订单号,商品号主键
decr key / decrby key num :递减 / 递减指定整数(value为整数才可用)
setnx key value :当key没有被赋值时才可被赋值,可以防止覆盖
append key value : 为指定key的value末尾追加value
strlen key : 获取指定key对应值的的长度
Hash(散列类型)
hset key field value hmset key field value field2 value2 :一次设置单个/多个值
hget key field hmget key field1 field2... :一次获取单个/多个值
hgetall key : 获取所有字段值
hsetnx key field value : 当field不存在时,插入值,当其存在时,不做操作
hdel key field1 field2 :删除单个/多个字段
hincrby key field num : 增加数字num 比如购物车中的购买数量
hkeys key / hvals key : 只获取字段名 / 字段值
hgetall key : 获取所有字段
一般用于储存那些对象数据,特别是对象属性经常发生增删改操作的数据(购物车)
如果采用String就得序列化回对象,修改了值再序列化存到redis,不高效
List ( 列表类型 )
Redis的列表类型可以储存一个有序的字符串列表,常用的操作是向列表两段添加元素,或者获得列表的某一个片段
列表类型内部使用的是双向链表实现的 增删快,查询慢,但是如果查询头部或者尾部的10条纪录也是极快的
lpush key v1 v2 v3 v4 rpush key v1 v2 v3 v4 :从列表左 / 右添加元素
lrange key start stop :查看列表某个区间的所有元素,索引从0开始
lpop key / rpop key :左 / 右 弹出一个元素(先移除再返回)
llen key :获取列表中元素的个数
因为储存是有序的,可以用来满足商品评论的需求,按照时间排序利用的就是插入有序这个特点
Set ( 集合类型 )
set类型即集合类型,其中的数据没有顺序,但是保证不重复
sadd key v1 v2 v3 :添加元素
srem key v1 v2 :删除指定元素
smembers key :获取集合中的所有的元素
sismember key v1 :判断元素是否在集合中
sdiff setA setB :求差集,属于setA 不属于setB的元素
sinter setA setB :求交集,属于setA 和setB的交集部分
sunion setA setB :求并集,setA 和 setB的并集
scard setA :获取集合中元素的个数
spop setA : 因为储存是无序的,所以是随机弹出一个元素
Zset(有序集合类型)
在集合类型的基础上,有序集合类型为集合中的每一个元素都关联了一个分数,这使得我们可以完成插入/删除和判断元素是否在集合中,还能够获得分数最高或者最低的前N个元素,或许指定分数范围内的元素等与分数有关的操作
Zest和List的区别:
列表类型是通过链表实现的,获取两端的数据很快,但是访问中间的数据会变慢很多
有序集合类型使用的事散列表实现,即使读取中间部分的数据也很快
列表中不能简单的调整某个元素的位置,但是有序集合可以通过更改分数实现调整
有序集合类型要比列表类型更耗内存
zadd key 分数 元素 比如:zadd stu 80 english :添加一个元素到有序集合,:英语元素 80分
zrange key start stop :按照元素分数从小到大,获得排名在某个范围的元素列表
zrevrange key start stop :按照元素分数从大到小,获得排名在某个范围的元素
withscores :跟在上面两种语法后,可以把元素的分数一并显示出来
zscore key 元素 :获取元素的分数
zrem key 元素 :删除元素
zincrby key num field :为field元素增加分数 num
zcard key :获得集合中元素的数量
zcount key min max :获取指定分数范围内的元素的个数
zremrangebyrank key start stop :按照排名范围删除元素
zremrangebyscoe key min max :按照分数范围删除元素
zrank / zrevrank key 元素 :从小->大 / 大->小获取元素的的排名
因为有一个分数,影响着我们元素的存放,商品销售排行榜就能利用这一点,分数表示销售数量
通用命令
keys pattern :获取所有有pattern样式的key
del / exists key :删除 / 判断是否存在 元素
expire key num :当作缓存数据库时,设置key的有效生存时间 单位秒
ttl / persist key : 查看key 的剩余有效时间 / 清楚生存时间
rename old new :重命名 key
type key :显示指定key的数据类型
Redis事务
redis事务介绍
-
rediss的事务是通过 multi 、 exec 、 discard 、warch 这四个命令来完成的
-
redis的单个命令都是原子性的,所以这里说的事务针对的是 : 命令集合
-
redis将命令集合序列化并确保处于同一事务的命令连续且不被打断的执行完毕
-
redis不支持事务回滚
相关命令
-
multi
用于标记事务块的开始
redis会将后续的命令逐个放入到队列中,然后使用 EXEC 命令原子化的执行这个命令序列
-
exec
在一个事务中执行先前所有放入到队列中的命令,然后恢复正常的连接状态
-
discard
清除所有执勤在一个事务中放入队列的命令,然乎恢复正常的连接状态
-
watch
当某个事务需要按条件执行时,就要使用这个命令将给定的键设置为受监控状态
使用该语法可实现redis的乐观锁 watch key [key....]
-
unwatch
清除所有先前为一个事务监控的键
事务失败处理
-
一种是语法错误,当我们真正用代码衔接的时候基本不会出现这种错误
-
另一种是类型错误,多种数据类型之间的转换造成的错误
-
redis不支持事务回滚,一方面是为了保持性能,另一方面就是上面两种错误都是我们程序员可以避免的
Redis实现分布式锁
-
单引用单进程多线程 : synchronize、Lock
-
分布式引用使用锁 :多进程
分布式锁的实现方式有几种
-
基于数据库的乐观锁实现分布式锁
-
基于zookeeper临时节点的分布式锁 ( 不懂 )
-
基于redis的分布式锁 (学习目标)
分布式锁的注意事项
-
互斥性 :在任意时刻,只有一个客服端可以持有锁
-
同一性 :加锁和解锁都必须是同一个客服端
-
可重入性:即使有一个客服端在持有锁时崩溃,也能保证后续的客服端能够加锁
实现分布式锁
Redis持久化方案
Redis作为一个内存数据库,通过定期将内存中数据刷到硬盘中,保证数据的持久性
简单带过,大部分我都知道 RDB 、AOF
RDB方式
这种方式redis会在指定的情况下触发快照,将数据持久化到硬盘,比如:
-
复合自定义配置的快照规则
-
执行save或者bgsave命令
-
执行flushall命令
-
执行主从复制操作
可以在redis.conf中设置自定义快照规则,比如:相互之间是 或 的关系
-
save 900 1 : 表示15分钟(900秒钟)内至少1个键被更改则进行快照。
-
save 300 10 : 表示5分钟(300秒)内至少10个键被更改则进行快照。
-
save 60 10000 :表示1分钟内至少10000个键被更改则进行快照。
快照的实现原理
-
redis使用fork函数赋值一份当前进程的副本(也就是创造一个子进程干事)
-
父进程继续处理客户端发来的各种请求,子线程刷数据到硬盘中
-
当子进程将数据刷完后,就会用该临时文件替换之前的RDB文件,操作完成
说明:
-
redis启动后会读取PDB快照文件,将数据从硬盘载入到内存中
-
在快照过程中,是创立一个临时文件装载,如果快照出现错误,也可以恢复以前的版本
-
一般我们可以通过定时备份RDB文件来实现redis数据库的备份,RDB文件占用空间小,传输也较快
-
使用RDF持久化方案,如果redis异常宕机,会丢失一部分数据,如果不能接受这部分损失,建议再开启AOF
-
RDB可以最大化Redis的性能,父进程不进行任何IO操作,但如果待持久化数据较大时,可能比较耗时
AOF方式
介绍:
-
AOF持久化方案需要手动开启 :appendonly yes
-
AOF持久化方案,记录的都每一条更改了Redis中数据的命令,写入一个AOF文件中,另外还值得一说的是,这个记录还会被优化比如你设置了一个值,然后你又对该键,设置了其他的值,就会产生覆盖,把之前的那个命令覆盖掉,从而使得被记载的命令是少的,打开过AOF文件的朋友都知道,AOF文件中的数据可读性很高
同步磁盘数据:
-
redis每次更改数据的时候,aof就会将命令记录到aof文件中,但命令并没有实时写入到硬盘,而是写到硬盘缓存中,硬盘缓存再将数据刷到硬盘
配置文件中的参数问题:
-
appendfsync always 每次执行写入都会进行同步: 最安全,但是效率太低
-
appendfsync everysec :每一秒刷一次 (推荐使用)
-
appendfsync no :不主动进行同步操作,不安全,不考虑
AOF文件 损害后以后如何修复:
-
在写入aof文件时,redis如果宕机造成AOF文件出错的话,redis在启动的时候会拒绝载入这个aof文件,确保数据不会被破坏
-
我们可以先备份当前损坏的aof文件,对其一个副本进行修复工作
-
使用redis自带的 redis-chech-aof程序,对aof文件进行修复:redis-check-aof--fix readonly.aof
-
重启redis,观察redis是否载入载入修复后的aof文件,如果成功读取 把备份的aofa文件删除即可
如何选择RDB 和 AOF
-
首先放到最前面,如果对于数据比较重要,建议两个持久化方式都开启
-
如果你能承受几分宗以内的数据丢失,可以只是哟个rdb,redis受性能影响小
-
不建议单独使用AOF持久化方式,RDB回复数据集的速度比AOF的速度快很多
-
之前的博客完全的记录过一次:https://www.cnblogs.com/msi-chen/p/10719083.html
Redis + LUA整合使用(不太懂)
LUA:是一种脚本语言,用C语言编写,功能为嵌放在应用程序中,为程序提供灵活的扩展和定制功能
Redis中使用LUA的好处
-
减少网络开销:可以把多个命令放在一个脚本中执行
-
原子操作 :Redis会将整个脚本作为一个整体执行,中间不会被其他命令打断
-
复用性 : 脚本保存在redis中,任何客户端都可以调度使用
LUA的安装
可以本地下载上传到linux,也可以使用curl命令在linux系统中进行在线下载
curl -R -O http://www.lua.org/ftp/lua-5.3.5.tar.gz
-
yum -y install readline-devel ncurses-devel
-
tar -zxvf lua-5.3.5.tar.gz
-
make linux
-
make install 最后,直接输入 lua命令即可进入lua的控制台
Redis + LUA整合使用
redis2.6版本后内置了Lua解释器,可以使用Eval对Lua脚本进行求值 格式如下
EVAL script numkeys key [key ...] arg [arg ...]
命令说明:
-
script参数:是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必(也不应该)定义为一个Lua函数。
-
numkeys参数:用于指定键名参数的个数。
-
key [key ...]参数: 从EVAL的第三个参数开始算起,使用了numkeys个键(key),表示在脚本中所用到的那些Redis键(key),这些键名参数可以在Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。
-
arg [arg ...]参数:,可以在Lua中通过全局变量argv数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。
比如:
-
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
-
"key1"
-
"key2"
-
"first"
-
"second"
lua脚本中调用redis命令
-
redis.call()
-
redis.pcall()
-
这两个函数的唯一区别在于它们使用不同的方式处理执行命令所产生的错误
-
示例: eval "return redis.call('set',KEYS[1],'bar')" 1 foo
-
OK
EVALSHA
-
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。
-
Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。
-
为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)
EVALSHA** 命令的表现如下:
-
如果服务器还记得给定的 SHA1 校验和所指定的脚本,那么执行这个脚本
-
如果服务器不记得给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用 EVAL 代替 EVALSHA
-
> evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0
"bar"
SCRIPT命令
l SCRIPT FLUSH :清除所有脚本缓存
l SCRIPT EXISTS :根据给定的脚本校验和,检查指定的脚本是否存在于脚本缓存
l SCRIPT LOAD :将一个脚本装入脚本缓存,返回SHA1摘要,但并不立即运行它
l SCRIPT KILL :杀死当前正在运行的脚本
redis-cli --eval
-
可以使用redis-cli 命令直接执行脚本,格式如下:
-
$ redis-cli --eval script KEYS[1] KEYS[2] , ARGV[1] ARGV[2] ...
Redis消息模式
队列模式
我们可以使用list类型的 lpush 和 rpop 实现消息队列
brpop 可以代替 rpop,如果消息队列中没有消息弹出,会一直阻塞,到时间了返回null,不会一直建立连接发送rpop命令
发布订阅模式
-
订阅消息( subscribe ) : subscribe test
-
发布消息( publish ) :publish test “我是Ninja”
缓存穿透、缓存击穿、缓存失效
-
对于redis作为缓存服务器,我们在查询数据数据的时候,优先访问redis是否有数据,没有的话在查询数据库
-
查询完数据库,获得结果的时候,除了返回给前端外,还得将查询结果写入redis缓存
缓存穿透
理解:
-
比如我们根据主键去查询数据,主键作为key在redis中缓存,当我们发起查询请求的时候,如果redis没有对应的值,那么查询就会建立数据库连接进行数据库查询,如果这个主键所关联的数据在数据库中也没有数据,并且对该主键查询的请求量还很大,就会对后端系统造成很大的压力,这就叫做缓存穿透
应对方案:
-
将对数据库查询为空的情况也进行缓存,只是缓存的时间设短一点,或者该key对应的数据有插入的时候清理缓存
-
做一个过滤,可以把所有的可能存在的key放到一个大的Bitmap中,查询时通过该bitmap过滤。(布隆表达式)
缓存雪崩
理解:
-
当缓存服务器重启,或者短时间内大量缓存的有效时间全部到期,所有请求都走数据库进行查询,给后端系统造成巨大压力的情况
应对方案
-
不同的key,设置不同的过期时间,让缓存失效的时间点精良均匀
-
在缓存失效后,通过加锁或者队列的方式控制读取数据库写缓存的线程数量,比如查询某个key,我们只允许一个线程去查询数据库,然后写缓存,其他线程等待,去缓存中获取数据
-
做二级缓存,一级缓存的有效时间短,二级缓存的有效期为长期,当一级缓存失效时访问二级缓存
缓存击穿(又名:热点Key)
理解:
-
缓存在某个过期的时候,恰好这个时候对与该key有大量的并发请求,这些请求全区后端数据库加载数据并写入到缓存,在成后端数据库瞬间崩塌,是不是缓存雪崩有点像?缓存击穿是针对某一个key ,缓存雪崩是针对多对key
应对方案:
使用redis的 setnx 互斥锁先进行判断,这样其他线程就处于等待状态,保证不会有大量并发请求去查询数据库就OJBK
或者使用双重检测锁解决这个问题:
-
-
如果为null ,我们就要上锁了,我们我都知道Spring容器默认是单列的,所以我以this为锁,
-
上锁之后,我们再次从缓存中获取缓存数据,目的是我们只需要一个请求去访问数据库,并将数据缓存到redis
-
其他请求处于等待队列中,稍后获取数据全是从redis缓存中获取,不会再去访问数据库,避免了热点key的问题
缓存淘汰策略之LRU
redis内置缓存淘汰策略
最大缓存 maxmemory = 0 ( 默认没设置大小 )
-
redis有最大缓存这一说法,也就是它的短板,默认没有最大值,当数据超过最大内存时,redis直接宕机
-
所以我们需要给他设置最大值,当数据达到最大值时,就会触发数据淘汰策略,保证redis不会宕机
淘汰策略:
-
redis淘汰策略配置:maxmemory-policy voltile-lru,支持热配置
redis提供六种数据淘汰策略:
-
voltile-lru:从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
-
volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
-
volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
-
allkeys-lru:从数据集(server.db[i].dict)中挑选最近最少使用的数据淘汰
-
allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
-
no-enviction(驱逐):禁止驱逐数据
LRU实现
-
常见的实现就是使用一个联表保存缓存的数据,新数据插入到链表头部
-
当缓存数据被访问的时候,就把该缓存数据移到链表头部
-
当链表要满的时候,将链表尾部的数据丢弃
了解了解
-
当存在热点数据时,LRU的效率很好,因为热点数据都在链表的头部部分,所以查询效率还是高但偶发性、周期性的批量操作会导致热点数据不在头部部分,LRU的命中里急剧下降,缓存污染比较严重,这样的话,命中就需要遍历链表,获得数据,再次将热点数据移到头部