- Redis是一个用C语言开发的高速缓存数据库,高级的key:value存储系统
- 缓存穿透:
- 指查询一个一定不存在的数据,由于缓存是不命中是需要从数据库查询,查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到数据库中去查询,造成缓存穿透。
- 解决方案:
- 最简单粗暴的方法:如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们就把这个空结果进行缓存,但它的过期时间会很短,最长不超过5分钟。
- Redis支持的数据类型:
- Strings(字符串)
- lists(字符串列表)
- hashes(字典)
- sets(字符串集合)
- zset(有序字符串集合)
- Key的建议
- key不要太长,尽量不要超过1024字节,这不经消耗内存,还会降低查询效率
- key不要太短,可读性会降低
- 在一个项目中,key最好使用统一 命名模式。
- String
set mystr "hello world!" //设置字符串类型
get mystr //读取字符串类型
127.0.0.1:6379> set mynum "2"
OK
127.0.0.1:6379> get mynum
"2"
127.0.0.1:6379> incr mynum
(integer) 3
127.0.0.1:6379> get mynum
"3"
- list
- redis中list底层实现上是一个链表,而不是数组,所以在列表中任意一个结点插入和删除元素的速度较快,但是对于大数据量的列表中,定位一个元素会比较慢。
- 常用操作:LPUSH、RPUSH、LRANGE
//新建一个list叫做mylist,并在列表头部插入元素"1"
127.0.0.1:6379> lpush mylist "1"
//返回当前mylist中的元素个数
(integer) 1
//在mylist右侧插入元素"2"
127.0.0.1:6379> rpush mylist "2"
(integer) 2
//在mylist左侧插入元素"0"
127.0.0.1:6379> lpush mylist "0"
(integer) 3
//列出mylist中从编号0到编号1的元素
127.0.0.1:6379> lrange mylist 0 1
1) "0"
2) "1"
//列出mylist中从编号0到倒数第一个元素
127.0.0.1:6379> lrange mylist 0 -1
1) "0"
2) "1"
3) "2"
- 集合set
- redis中的集合时无序的,集合中的元素没有先后顺序
- 相关操作:添加新元素、删除已有元素、取交集、并集、取差
//向集合myset中加入一个新元素"one"
127.0.0.1:6379> sadd myset "one"
(integer) 1
127.0.0.1:6379> sadd myset "two"
(integer) 1
//列出集合myset中的所有元素
127.0.0.1:6379> smembers myset
1) "one"
2) "two"
//判断元素1是否在集合myset中,返回1表示存在
127.0.0.1:6379> sismember myset "one"
(integer) 1
//判断元素3是否在集合myset中,返回0表示不存在
127.0.0.1:6379> sismember myset "three"
(integer) 0
//新建一个新的集合yourset
127.0.0.1:6379> sadd yourset "1"
(integer) 1
127.0.0.1:6379> sadd yourset "2"
(integer) 1
127.0.0.1:6379> smembers yourset
1) "1"
2) "2"
//对两个集合求并集
127.0.0.1:6379> sunion myset yourset
1) "1"
2) "one"
3) "2"
4) "two"
- 有序集合 sorted sets
- 有序集合中的每个元素都关联一个序号(score),这就是排序的依据。
- 很多时候,我们都将redis中有序集合叫做zsets,因为有序集合相关操作指令都是以z开头的:zrange,zadd , zrevrange , zrangebyscore
127.0.0.1:6379> zadd myzset 1 baidu.com
(integer) 1
//向myzset中新增一个元素360.com,赋予它的序号是3
127.0.0.1:6379> zadd myzset 3 360.com
(integer) 1
//向myzset中新增一个元素google.com,赋予它的序号是2
127.0.0.1:6379> zadd myzset 2 google.com
(integer) 1
//列出myzset的所有元素,同时列出其序号,可以看出myzset已经是有序的了。
127.0.0.1:6379> zrange myzset 0 -1 with scores
1) "baidu.com"
2) "1"
3) "google.com"
4) "2"
5) "360.com"
6) "3"
//只列出myzset的元素
127.0.0.1:6379> zrange myzset 0 -1
1) "baidu.com"
2) "google.com"
3) "360.com"
- 哈希
- 哈希是redis-2.0版本以后才有的数据结构
- hashes是存储字符串和字符串值之间的映射
//建立哈希,并赋值
127.0.0.1:6379> HMSET user:001 username antirez password P1pp0 age 34
OK
//列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "P1pp0"
5) "age"
6) "34"
//更改哈希中的某一个值
127.0.0.1:6379> HSET user:001 password 12345
(integer) 0
//再次列出哈希的内容
127.0.0.1:6379> HGETALL user:001
1) "username"
2) "antirez"
3) "password"
4) "12345"
5) "age"
6) "34"
- 支持的Java客户端有 Redisson,jedis,lettuce
- Redis的持久化的两种策略:
- Redis分布式锁
Redis分布式锁其实就是在系统里面占一个“坑”,其他程序也要占坑的时候,占用成功了就可以继续执行,失败了就只能放弃或稍后重试。
占坑一般使用setnx(set if not exists)指令,只允许被一个程序占有,使用完调用del释放锁。
redis分布式锁不能解决超时的问题,分布式锁有一个超时时间,程序的执行如果超出了锁的超时时间就会出现问题。
2.Redis的散列表(建议)
把相关的信息放到散列表里面存储,而不是把每个字段单独存储,这样可以有效的减少内存使用。比如讲Web系统的用户对象,应该放到散列表里面再整体存储到Redis,而不是把用户的姓名、年龄、密码、邮箱等字段分别设置Key进行存储。
- volatile-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(驱逐):禁止驱逐数据。
- redis的两种持久化方式:RDB(Redis DataBase) 和AOF(Append Only File)
- RDB:就是在不同的时间点,将redis存储的数据生成快照 并存储到磁盘等介质上。
- AOF:则是换一个角度来实现持久化,那就是将redis执行过的所有指令记录下来,下次redis重新启动时,只要把这些指令从前到后再重复执行一遍,就可以实现数据恢复了。
- 其实RDB和AOF两种方式也可以同时使用,在这种情况下,如果redis重启的话,则会优先采用AOF进行数据恢复,这是因为AOF方式的数据恢复完整度更高。
- 如果没有数据持久化的需求,也可以完全关闭RDB和AOF方式,这样的话,redis将变成一个纯内存数据库。
- RDB:
- 将redis某一时刻的数据持久化到磁盘中,是一种快照式的持久化方法。
- redis在进行数据持久化的过程中,会先将数据写入到一个临时文件中,待持久化过程都结束了,才会用到这个临时文件替换上次持久化好的文件,正式这种特性,让我们可以随时进行备份,因为快照文件总是完整可用的。对于RDB方式,redis会单独创建fork一个子进程来进行持久化,而主进程是不会进行任何IO操作,这样就确保了redis极高的性能。如果需要大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那么RDB方式就不太适合。因为即使你每5分钟都持久化一次,当redis故障时,忍让会有近5分钟的数据丢失,所以redis还提供了另一种持久化方式,那就是AOF。
- AOF:
- 只允许追加不允许改写的文件,将执行过的写指令记录下来,在数据恢复时按照从前到后的顺序再将指令执行一遍
- 通过配置redis.conf中的appendonly yes 就可以打开AOF功能,如果有写操作,redis就会追加到AOF文件的末尾。
- 默认的AOF持久化策略是每秒钟把缓存中的写指令记录到磁盘中,因为这种情况下,redis仍然可以保持很好的处理性能,即使redis故障,也只会丢失最近1秒钟的数据。
- 如果在追加日志时,恰好遇到磁盘空间满,inode满或断点等情况导致日志写入不完整,redis提供redis-check-aof工具进行日志恢复。
- 因为采用了追加方式,如果不做任何处理的话,AOF文件会变得越来越大,Redis提供了AOF文件重写rewrite机制,即当AOF文件的大小超过所设定的阈值时,redis就会启动AIF文件的内容压缩,只保留可以恢复数据的最小指令集。
- 假如调用了100次INCR指令,在AOF文件中就要存储100条指令,但是这明显是很抵消的,完全可以吧100条指令合并成一条set指令,这就是重写机制的原理。
- redis的事务处理
- MULTI :用来组装一个事务
- EXEC:用来执行一个事务
- DISCARD:用来取消一个事务
- WATCH:用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
redis> MULTI //标记事务开始
OK
redis> INCR user_id //多条命令按顺序入队
QUEUED
redis> INCR user_id
QUEUED
redis> INCR user_id
QUEUED
redis> PING
QUEUED
redis> EXEC //执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
- WATCH
- 用于监视key是否被改动过,而且支持同时监视多个key,只要还没真正触发事务,WATCH都会尽职尽责的监视,一旦发现某个key被修改了,在执行EXEC时就会返回nil,表示事务无法触发。
127.0.0.1:6379> set age 23
OK
127.0.0.1:6379> watch age //开始监视age
OK
127.0.0.1:6379> set age 24 //在EXEC之前,age的值被修改了
OK
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 25
QUEUED
127.0.0.1:6379> get age
QUEUED
127.0.0.1:6379> exec //触发EXEC
(nil) //事务无法被执行
- 最常见的关于事务的错误
- 调用EXEC之前的错误
- 调用EXEC之后的错误
- 调用EXEC之前的错误,有可能是由于语法有误导致的,也有可能是内存不足导致,只要出现某个命令无法成功写入缓冲队列的情况,redis都会进行记录,在客户端调用EXEC时,redis会拒绝执行这一事务。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> haha //一个明显错误的指令
(error) ERR unknown command 'haha'
127.0.0.1:6379> ping
QUEUED
127.0.0.1:6379> exec
//redis无情的拒绝了事务的执行,原因是“之前出现了错误”
(error) EXECABORT Transaction discarded because of previous errors.
- 调用EXEC之后的错误,redis采取了完全不同的策略,即redis不会理睬这些错误,而是继续向下执行事务中的其他命令,这是因为,对于应用层面的错误,并不是redis自身需要考虑和处理的问题,所以一个事务中如果某一条命令执行失败,并不会影响接下来的其他命令的执行。
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set age 23
QUEUED
//age不是集合,所以如下是一条明显错误的指令
127.0.0.1:6379> sadd age 15
QUEUED
127.0.0.1:6379> set age 29
QUEUED
127.0.0.1:6379> exec //执行事务时,redis不会理睬第2条指令执行错误
1) OK
2) (error) WRONGTYPE Operation against a key holding the wrong kind of value
3) OK
127.0.0.1:6379> get age
"29" //可以看出第3条指令被成功执行了
- redis为什么这么快?
- 纯内存操作
- 单线程操作,避免了频繁的上下文切换
- 采用了非阻塞I/O多路复用机制
- redis五中数据类型主要用途
- String:最常规的get、set操作,Value可以是String也可以是数字,一般做一些复杂的计数功能的缓存
- hash:这里的value存放的结构化的对象,比较方便操作其中某个对象,常用于单点登录,用这种数据结构存放用户信息,以cookie作为key,设置30分钟为缓存过期时间,很好的模拟session。
- list:可用作简单的消息队列,也可以用lrange,做基于redis 的分页功能,响应速度简直不要太快。
- set:set存储的是不重复的值,所以可以用做全局去重的功能,相较于JVM自带的set,效率更好。
- sorted set:多了一个权重参数score ,集合中的元素可以按score进行排列,可以做排行榜操作,取TOP N。
- redis的过期策略及内存淘汰机制:
- 分析:
- 假设你的redis只能存储5个G的数据,可是你写了10个G,由于只有一个redis服务器,那么会删除5G的数据,该如何删除?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,该如何解决?
- redis采用的是定期删除 + 惰性删除策略。
- 为什么不用定时删除策略?
- 定时删除,用一个定时器来负责监听key,过期则自动删除,虽然内存及时释放,但是十分消耗CPU资源。在打并发请求下,CPU要将时间应用在处理请求,而不是删除key。因此不适合采用定时删除策略。
- 定期删除+惰性删除是如何工作的?
- 定期删除,redis默认每隔100ms检查,是否有过期的key,有过期key则删除。需要说明的是,redis不是每个100ms将所有的key检查一次,而是随机抽取进行检查。因此如果只采用定期删除策略,同时也会导致很多key到时间没有被删除掉。所以还要搭配惰性删除。惰性删除,就是在获取某个key的时候,redis会检查一下,这个key如果设置了过期时间,那么是否过期了,如果过期了,此时就会删除。
- 采用定期删除+ 惰性删除就没其它问题了?当然不是,如果定期删除没删除key,且你也没及时去请求key,也就是惰性删除也没有生效。这样redis内存就会越来越高,那么需要采用内存淘汰机制。在redis-conf中配置
- # maxmemory-policy volatile-lru
- noeviction: 当内存不足以容纳新写入数据时,新写入操作就会报错。不推荐!
- allkeys-lru: 当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的key。推荐使用。
- allkeys-random: 当内存不足以容纳新写入数据时,在键空间中,随机移除某个key.不推荐
- volatile-lru: 当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,移除最少使用的key。这种情况一般是把redis即当做缓存,同时又做持久化存储的时候才用。
- volatile-ttl :当内存不足以容纳新写入数据时,在设置了过期时间的键空间中,有更早过期时间的key优先移除。
- 如果没有设置expire的key,不满足先决条件(prerequisites),那么volatile-lru , volatile-random 和volatile-ttl 策略的行为和noeviction(不删除) 基本上一致。
- 假设你的redis只能存储5个G的数据,可是你写了10个G,由于只有一个redis服务器,那么会删除5G的数据,该如何删除?还有,你的数据已经设置了过期时间,但是时间到了,内存占用率还是比较高,该如何解决?
- 分析: