redis
含义
基于内存的键值(key-value )存储,都是单线程操作,常被用于缓存,提高数据读写速度,减轻对数据库存储与访问压力,但不是普通缓存中间件,适合用在:高并发,低延迟,需要原子计数或者复杂数据结构的应用。
redis的性能极高,目前读可以达到十五万,写可以达到十万
数据类型及命令
String类型(字符串,数字):
dart
命令格式:
set key value //存入键值对
get key //根据键取出值
incr key //把值递增1
decr key //把值递减1(这个只记递增也可以,key设成负数)
del key //根据键删除键值对
setex key timeout value //存入键值对,但有时效,timeout表示失效时间,单位s
ttl key //可以查询当前的key还剩余多长时间过期
setnx key value //如果key存在,不做操作,如果key不存在,就存入键值对
以下为不常用命令:
incrby key num //偏移值,可以做批量增删,是incr做不到的
mset k1 v1 k2 v2 //批量存入键值对,可以加入多个
mget k1 k2 //批量取出键值,可以取出多个
append key 'value' //在键对应的原值后面拼接新内容value
setrange key index value
//修改键对应位置的值,index表示开始索引位置。
//如果新值长度大于索引开始的原值长度,就直接向后追加全部内容;
//如果小于,就只覆盖新值长度,原值没被覆盖的地方保持不变;
//如果index大于原值长度,空的位置会用零字节补全,但是最好不要这么勇,数据容易出错
- String类型常用操作场景:缓存对象,计数器,限流,验证码
Hash类型(字段-值映射):
dart
命令格式:
hset key hashkey hashvalue //存入一个键值对,值同样也是一个键值对
hget key hashkey //根据hash对象的键来获取对应的值
hexists key hashkey //判断hash对象是否含有这个键
hdel key hashkey //根据hash对象键删除该键值对
以下为不常用命令:
hincrby key hashkey num //给hashkey对应的值增加偏移量,可以是正负数,就代表加或者减
hlen key //获取hash对象键的数量
hkeys key //获取该对象中所有的键
hvals key //获取该对象中所有的值
hgetall key //获取该对象中所有的数据
hsetnx key hashkey hashvalue //和setnx一样,没有就添加,有就不操作
- Hash类型常用操作场景:用户信息,购物车,对象缓存
List类型(双向链表):
arduino
命令格式:
rpush key value //往列表的右边添加数据,多个数据中间用空格隔开
lrange key start end //范围显示列表数据,显示完整数据设置0 -1
lpush key value //往列表的左边添加数据,规则同上
lpop key //弹出列表最左边的数据,同时列表中失去这个数据
rpop key //弹出列表最右边的数据,同时列表中失去这个数据
llen kehy //获取列表的长度
以下为不常用命令:
linsert key before/after refVal newVal //在参考值之前/之后插入数据
lset key index value //根据index索引修改对应的数据
lrem key count value
//count大于0,从左往右找count个数据值为value的,删除
//count小于0,从右往左找|count|个数据值为value的,删除
//count=0,删除所有数据值等于value的
ltrim key start end // 根据范围截取列表,列表就会只剩这个范围的数据
lindex key index //根据索引获取列表中的数据
- List类型常用操作场景:简单队列,时间线,消息列表
Set类型(无序去重集合):
csharp
命令格式:
sadd key value //往set集合里面添加元素
smembers key //列出集合中所有元素
srem key value //删除集合中的某个元素
spop key count //随机弹出集合中的元素,弹出后集合中不再包含
sdiff key1 key2 //得到key1中特有元素(属于差集)
sinter key1 key2 //得到两个集合中的共同包含的元素(交集)
sunion key1 key2 //得到两个集合中的全部元素(并集)
scard key //返回集合集合中的元素个数
以下为不常用命令:
sdiffstore var key1 key2 //得到差集的元素,存入另一个set集合(var)
sinterstore var key1 key2 //得到交集的元素,存入另一个set集合
sunionstore var key1 key2 //得到并集的元素,存入另一个set集合
smove key1 key2 value //把key1中的某个元素移动到key2的集合中
sismember key value //判断该集合中是否包含某个元素
srandmember key count //随机获取集合中的某个元素,原集合当中依然存在
- Set类型常用操作场景:去重,共同关注,随机抽奖
Zset(sorted_set,有序集合):
arduino
命令格式:
zadd key score column //存入分数和名称,分数就是权重
zincrby key num column //给分数添加对应的偏移量,依旧是可以正负数,代表加减
zrange key start end //按照分数升序输出名称
zrevrange key start end //按照分数降序输出名称
zrank key name //按照升序返回这个名称对应的排名,都是从0开始算
zrevrank key name //按照降序返回这个名称对应的排名
zcard key //返回元素个数
以下为不常用指令:
zrangebyscore key min max [withscores] //按照分数范围升序输出名称,withscores表示带着分数输出,可选可不选
zrevrangebyscore by max min [withscores] //按照分数范围降序输出名称
zrem key name //根据名称删除对应的元素
zremrangebyscore key min max [withscores] //根据分数范围删除元素
zremrangebyrank key start end //根据排名返回删除元素
zcount key min max //根据分数范围统计个数
- Zset类型常用操作场景:排行榜,延时队列
总结
- 如果要排序选用zset
- 如果数据是多个且允许重复选用list
- 如果数据是多个且不允许重复选用set
- 剩下的使用string
- 如果是对象类型也可使用hash
设计key注意:唯一性,可读性,灵活性,时效性
设计value注意:根据项目需求来决定
注:ttl查询剩余时间,-2代表这个key是永久的,没有设置过期时间,永远都不会过期,-1代表这个key不存在,可能是已经过期了,也可能是最开始根本没有创建
使用redis的高级命令
arduino
常用指令:
keys * //可以用来模糊查询,就比如keys user:*,就代表查询的是user:开头的
exists //查询是否存在某个key
expire //设置某个key的过期时间,可以根据ttl来查看剩余时间
persist //取消过期时间
flushdb //清空当前redis数据库
flushall //清空所有redis数据库
dbsize //查看redis数据库中的key数量
以下是不常用指令:
select //选择对应数据库 0-15
move [key] [数据库下标] //把对应的key移动到其他数据库中
randomkey //随机返回数据库里的一个key,数据库中仍然存在
rename //给对应的key重命名
echo //打印,后面写什么就打印什么
info //获取数据库信息
config get * //返回所有配置
注:keys * 只有在个人测试 的时候可以使用,在数据量很大的情况下去遍历会阻塞redis!
redis的登录最好设置一个很复杂的密码,不然很容易被破解,一秒钟可以尝试十五万次
redis-cli -a 密码
redis的事务
arduino
由四个命令实现:
MULTI // 开启事务,开始排队命令
EXEC // 依次执行排队中的命令
DISCARD // 取消事务,丢弃所有排队命令
WATCH // 监视 key,用于乐观锁
UNWATCH // 取消监视
注:redis事务不支持回滚,即不会撤销之前已经执行命令,也不会跳过后面未执行的命令
WATCH用来监视对应的key,必须配套使用,进行到EXEC的时候,如果被监视的key被其他客户端修改了,则返回(nil),事务就不会执行。
redis持久化机制
分为RDB方式和AOF方式
RDB是把某一时刻的内存数据压缩保存为一个二进制文件,默认为dump.rdb;是按照条件生成"快照"
可以有对应的触发条件,如下,多个条件满足任意一个就会触发BGSAVE(异步),这是自动触发
bash
# 900 秒内至少有 1 次修改
save 900 1
# 300 秒内至少有 10 次修改
save 300 10
# 60 秒内至少有 10000 次修改
save 60 10000
工作流程是:满足条件触发后,先fork(拷贝)一份子进程,子进程把当前内存数据写成RDB文件,写完后替换为dump.rdb,就是RDB保存的默认文件,就是保存成功了,最后通知主进程保存完成。
注:fork完成,主进程是继续正常运行的;写入期间父进程如果修改内存页,会通过Copy On Write(写时复制)复制该页;RDB文件写入完成后通过rename原子替换。
优点:文件占用空间小;恢复速度快,启动时直接加载快照;对redis正常读写性能影响较小
缺点:数据丢失窗口大,两次快照之间的数据可能会全部丢失;fork可能造成顺势阻塞,内存页写入多时内存占用可能翻倍;不适合需要分钟秒级的恢复数据
AOF是把每条修改数据的命令以redis协议格式追加到文件末尾,默认文件为appendonly.aof;本质上就是命令日志
在AOF的配置方面,有一个fsync策略,由三种配置
perl
always:每条命令都执行fsync
everysec:每秒执行一次fsync
no:由操作系统觉得合适刷盘
在生产环境中通常会使用第二种,everysec,对比起来性价比最高,最多丢一秒数据
AOF会无限追加,文件会越来越大,redis会执行AOF Rewrite,用当前数据生成一份最小的,替换旧文件,而它也有对应的触发条件,两个都满足的情况下,就触发
arduino
# 比上次重写后增长了 100%
auto-aof-rewrite-percentage 100
# 当AOF文件超过 64MB
auto-aof-rewrite-min-size 64mb
重写工作流程是:依旧是满足条件触发重写之后,先fork一份子进程,子进程基于当前内存生成一份AOF文件,主进程继续接收写命令,并缓存到内存,新文件写完后,把缓存中的命令追加到新文件,原子替换旧AOF文件
启动恢复时:如果开了AOF,优先加载AOF文件,如果没开,再加载RDB文件,因为AOF通常比RDB数据更完整
优点:数据更安全;文件可读,便于人工检查;适合作为恢复和审计手段
缺点:文件比RDB大;恢复速度比RDB慢,高并发写入对磁盘有一定压力
redis的内存淘汰机制
含义:redis内存达到上限之后,在还没有过期的key当中,通过一些方式挑选出key删除来释放内存
前提:要配置maxmemory,默认内存不设上限,只有设了上限,内存达到上限才会触发淘汰
八种淘汰策略
noeviction:不淘汰任何 key;内存满了后,写命令返回 OOM 错误(默认策略)allkeys-lru:所有 key 中淘汰最近最少使用 -- 主流选择volatile-lru:只从设置了过期时间的 key 中淘汰最近最少使用allkeys-random:所有 key 中随机淘汰volatile-random:只在有过期时间的 key 中随机淘汰volatile-ttl:只在有过期时间的 key 中淘汰剩余 TTL 最短的allkeys-lfu:所有 key 中淘汰访问频率最低 -- 主流选择volatile-lfu:只在有过期时间的 key 中淘汰访问频率最低
过期key处理机制
注:过期时间到期不代表key立刻从内存当中消失
- 惰性删除 :当客户端访问key时,redis先检查key是否过期,如果过期就删除,当作key不存在处理,如果没过期就正常返回。
- 优点:节省cpu
- 缺点:如果过期key一直不被访问,就会一直占内存
- 定期删除:redis会周期性的从设置了过期时间的key中查询并删除已经过期的key,而不用去遍历所有的key;如果多起key比例依然很高,就多轮采样
注:大多数情况是两种删除机制配合使用
redis缓存三大经典问题
本质上都是缓存没抗住请求,压力到了数据库
- 缓存穿透
- 查询的key在缓存和数据库中都不存在,所以请求会绕过缓存,直接达到数据库
- 如果大量不存在的id同时请求,数据库会崩
- 解决方法:
- 缓存空值:数据库查不到时,也把空结果缓存 1 到 5 分钟。
- 布隆过滤器:查询前先判断 ID 是否存在,过滤器说不存在就直接返回。
- 参数校验:拦截非法 ID、超长参数、明显异常请求。
- 限流:对高频无效请求做限流和封禁。
- 缓存击穿
- 某个热点key正好过期,同一时间大量请求都来查这个key,数据库承受巨大压力
- 和穿透的区别:穿透是数据本来就不存在;击穿是数据存在,但key过期了
- 解决方法:
- 互斥锁:只让一个请求回源查数据库并重建缓存,其他请求等待。
- 逻辑过期:缓存不真正过期,值里带一个过期时间,发现逻辑过期后异步刷新。
- 后台定时刷新:热点 key 永不过期,由后台任务主动更新。
- 本地缓存兜底:Redis 之上的本地缓存再挡一层。
- 预热:活动开始前提前把热点 key 加载进缓存。
- 缓存雪崩
- 两种含义:1.大量key在同一时间段集中过期;2.redis实例宕机,不可用
- 解决方法:
- 过期时间加随机值:TTL = 基础时间 + random(0, 300)
- 多级缓存:本地缓存 + Redis,减少直接打 DB。
- Redis 高可用:Sentinel、Cluster,避免单点宕机。
- 熔断、降级、限流:Redis不可用时,让部分请求快速失败或走降级数据。
- 预热:上线前把热点数据提前加载。
- DB 保护:数据库连接池限流、读写分离。
实际运用
注:此处用java举例
1.jedis
- 首先要在pom.xml里面导入有关jedis的maven依赖(在其他正常配置都完成的状态下)
xml
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 然后就可以在src里创建自己的包,创建一个新类
- 以下代码就是简单尝试jedis的测试
java
public class TestJedis {
public static void main(String[] args) {
//创建jedis连接池
JedisPool pool = new JedisPool("localhost", 6379);
//拿到jedis对象
Jedis jedis = pool.getResource();
//设置密码,没有就跳过
jedis.auth("123456");
//操作redis,调用的方式和命令行中的命令是一个名字
jedis.set("str","hello redis");
jedis.set("num","10");
jedis.incrBy("num",1);
jedis.hset("user","name","xiaoming");
jedis.hset("user","age","18");
Map<String, String> map = new HashMap<>();
map.put("name","xiaowang");
map.put("age","10");
//hset不允许map里面存多个,如果要存入多个一起传就用hmset
jedis.hmset("user2",map);
jedis.rpush("hobby","java","python","c","php");
jedis.sadd("java","java1","java2","java3","java4","java5");
jedis.zadd("score",80,"xiaoming");
jedis.zadd("score",20,"xiaowang");
jedis.zadd("score",90,"xiaomei");
//释放资源
jedis.close();
pool.destroy();
}
}
2.SpringBoot
- 首先还是要导入对应依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 其次在资源配置的部分添加有关redis的配置
properties
spring.application.name=testjedis
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=123456
- 最后就是在test包里面创建自己的包,再创建一个测试类
- 以下代码是用集成SpringBoot的方式来测试redis
java
@SpringBootTest
class TestjedisApplicationTests {
@Autowired
private StringRedisTemplate redisTemplate;
@Test
void contextLoads() {
//操作字符串
redisTemplate.opsForValue().set("name","xx");
redisTemplate.opsForValue().set("num","10");
redisTemplate.opsForValue().increment("num",10);
//setnx操作,虽然是一句话,但是在redis里是两句话,容易出现脏数据
redisTemplate.opsForValue().setIfAbsent("num","10",30, TimeUnit.SECONDS);
//操作hash类型数据
redisTemplate.opsForHash().put("user","name","xx");
redisTemplate.opsForHash().put("user","age","10");
Map<Object,Object> map = new HashMap<>();
map.put("name","xx");
map.put("age","10");
redisTemplate.opsForHash().putAll("usertrue",map);
//操作list类型数据
redisTemplate.opsForList().rightPushAll("arr","a","b","c","d");
String arr = redisTemplate.opsForList().leftPop("arr");
System.out.println(arr);
//操作set类型
redisTemplate.opsForSet().add("num","10","9","8");
List<String> num = redisTemplate.opsForSet().pop("num", 2);
for (String s : num) {
System.out.println(s);
}
//操作zset
redisTemplate.opsForZSet().add("z","java",80);
redisTemplate.opsForZSet().add("z","java77",87);
redisTemplate.opsForZSet().add("z","java56",99);
Set<String> range = redisTemplate.opsForZSet().rangeByScore("z", 60, 100);
for (String s : range) {
System.out.println(s);
}
Set<String> keys = redisTemplate.keys("*");
for (String s : keys) {
System.out.println(s);
}
}