这是我参与「第五届青训营 」伴学笔记创作活动的第 13 天
Redis
为什么需要Redis
-
数据从单表演进出分库分表
-
mySQL从单机演进到集群
- 数据量增长
- 读写数据压力的不断增加
-
数据分冷热
- 热数据:经常被访问的数据
-
将热数据存储到内存中
Redis基本工作原理
-
数据从内存中读写
-
数据保存到硬盘上防止重启数据丢失
- 增量数据保存到AOF文件(同步)
- 全量数据保存到RDB文件
- 单线程处理操作命令
RDB(Redis DateBase)
Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入一个临时文件中,等持久化过程结束后替换上次持久化的文件。
进行大规模的数据恢复RDB比AOF更加高效,因为不进行IO操作
缺点:最后一次持久化的数据可能会丢失,默认是RDB
rdb保存的文件是dump.rdb
127.0.0.1:6379> config get dir
1) "dir"
2) "D:\Environment\Redis-x64-3.2.100" #如果在这个目录下存在dump.rdb文件,启动就自动恢复数据
优点:
1、适合大规模的数据恢复
2、对数据的完整性要求不高
缺点:
1、需要一定的时间间隔进程操作,redis意外宕机了最后一次数据就没有
2、fork进程的时候会占用一定的空间
AOF(Append Only File)
aof文件大于64M,fork新进程重写文件!将所有命令都记录下来。
默认不开启:
appendonly no #改为yes就开启
appendfilename "appendonly.aof" #持久化的文件名
appendfsync always #每次修改都会sync 消耗性能
appendsync everysec #每秒执行一次sync
appendsync no #不执行sync,操作系统自己同步数据
如果aof文件有错误,redis是启动不起来,利用
redis-check-aof --fix appendonly.aof修复文件
缺点:
1、相对于数据文件来说,aof远远大于rdb,修复速度比rdb慢!
2、aof运行效率慢!有IO操作!
扩展:
- 只做缓存可以不适用任何持久化
- 同时开启两种持久化方式,优先载入AOF,因为数据更完整
Redis案例说明
1.签到案例(String数据结构)
先熟悉一下redis的命令
127.0.0.1:6379> setex key1 30 "hello" //设置key过期时间 30s后过期
OK
127.0.0.1:6379> ttl key1
(integer) 26
127.0.0.1:6379> set views 0 //设置浏览量
OK
127.0.0.1:6379> incr views //增加1
(integer) 1
127.0.0.1:6379> get views
"1"
127.0.0.1:6379> incrby views 10 //自增 设置步长
(integer) 11
127.0.0.1:6379> EXPIRE age 10 //单独设置key过期时间 单位s
(integer) 1
127.0.0.1:6379> mset k1 v1 k2 v2 k3 v3 //同时设置多个值
OK
127.0.0.1:6379> keys *
1) "k3"
2) "k2"
3) "k1"
127.0.0.1:6379> mget k1 k2 k3 //同时获取多个值
1) "v1"
2) "v2"
3) "v3"
redisTemplate
redisTemplate.boundValueOps("StringKey").set("StringValue",1, TimeUnit.MINUTES); //设置kv并设置过期时间
ValueOperations ops = redisTemplate.opsForValue();
ops.set("StringKey", "StringVaule"); //通过opsForValue设置
//单独设置过期时间
redisTemplate.expire("StringKey",1,TimeUnit.MINUTES);
//获取缓存值
String str1 = (String) redisTemplate.boundValueOps("StringKey").get();
redisTemplate.opsForValue().get("StringKey");
//删除key
redisTemplate.delete("StringKey");
//递增
redisTemplate.boundValueOps("StringKey").increment(3L);
2.消息通知(list作为消息队列)
127.0.0.1:6379> lpush list one #左插值
(integer) 1
127.0.0.1:6379> lpush list two
(integer) 2
127.0.0.1:6379> lpush list three
(integer) 3
127.0.0.1:6379> lrange list 0 -1 #通过区间获取具体的值
1) "three"
2) "two"
3) "one"
127.0.0.1:6379> lrange list 0 1
1) "three"
2) "two"
127.0.0.1:6379> rpush list right #右插值
(integer) 4
127.0.0.1:6379> lpop list #左弹值
"three"
127.0.0.1:6379> rpop list #右弹值
"right"
127.0.0.1:6379> lindex list 0 #通过下表获得list中的某个值
"two"
127.0.0.1:6379> llen list #获得list的长度
(integer) 2
127.0.0.1:6379> lrem list 1 one #移除list集合中指定个数的value
(integer) 1
127.0.0.1:6379> lset list 0 okk #替换当前索引位置的值,若无值会报错
OK
redisTemplate
redisTemplate.boundListOps("listKey").leftPush("listLeftValue1"); //左插值
redisTemplate.boundListOps("listKey").rightPush("listRightValue2"); //右插值
redisTemplate.opsForList().leftPush("listKey", "listLeftValue3");
//将list放入缓存
ArrayList<String> list = new ArrayList<>();
redisTemplate.boundListOps("listKey").rightPushAll(list);
redisTemplate.boundListOps("listKey").leftPushAll(list);
//设置过期时间
redisTemplate.expire("listKey",1,TimeUnit.MINUTES);
//获取全部内容
List listKey1 = redisTemplate.boundListOps("listKey").range(0, 10);
//根据索引查询元素
String listKey4 = (String) redisTemplate.boundListOps("listKey").index(1);
//获取list长度
Long size = redisTemplate.boundListOps("listKey").size();
3.计数(hash数据结构)
文章点赞数/文章被阅读数
127.0.0.1:6379> hset myhash field liuxiang #set一个具体的key-value
(integer) 1
127.0.0.1:6379> hget myhash field
"liuxiang"
127.0.0.1:6379> hgetall myhash #获取全部的数据
1) "field"
2) "hello"
3) "field1"
4) "world"
127.0.0.1:6379> hdel myhash field1 #删除指定字段及value值
(integer) 1
127.0.0.1:6379> hlen myhash #获取hash表的字段数量
(integer) 1
127.0.0.1:6379> hkeys myhash #获取所有字段
1) "field"
127.0.0.1:6379> hvals myhash #获取所有值
1) "hello"
127.0.0.1:6379> hincrby myhash field1 1 #自增
(integer) 2
redisTemplate
//添加缓存
redisTemplate.boundHashOps("HashKey").put("SmallKey", "HashVaue");
HashOperations hashOps = redisTemplate.opsForHash();
hashOps.put("HashKey", "SmallKey", "HashVaue");
//设置过期时间
redisTemplate.expire("HashKey",1,TimeUnit.MINUTES);
//添加一个map集合
HashMap<String, String> hashMap = new HashMap<>();
redisTemplate.boundHashOps("HashKey").putAll(hashMap );
//获得所有key
Set keys1 = redisTemplate.boundHashOps("HashKey").keys();
//获得所有value
List values1 = redisTemplate.boundHashOps("HashKey").values();
//根据key提取value
String value1 = (String) redisTemplate.boundHashOps("HashKey").get("SmallKey");
//删除
redisTemplate.boundHashOps("HashKey").delete("SmallKey");
//判断是否右该值
Boolean isEmpty = redisTemplate.boundHashOps("HashKey").hasKey("SmallKey");
Hash数据结构
- rehash:槽位不够用时,需要扩容,直接将h[0]拷贝到h[1],数据量大时会造成阻塞
- 渐进式rehash:每次用户访问时都会迁移少量数据
4.排行榜(zset有序集合数据结构)
底层:跳表,通过key操作调表的功能
127.0.0.1:6379> zadd salary 2500 xiaohong #添加用户
(integer) 1
127.0.0.1:6379> zadd salary 3000 xiaoming
(integer) 1
127.0.0.1:6379> zadd salary 500 xiaocong
(integer) 1
127.0.0.1:6379> zrangebyscore salary -inf inf #显示全部用户从小到大
1) "xiaocong"
2) "xiaohong"
3) "xiaoming"
127.0.0.1:6379> zrangebyscore salary -inf inf withscores #显示用户并带成绩
1) "xiaocong"
2) "500"
3) "xiaohong"
4) "2500"
5) "xiaoming"
6) "3000"
127.0.0.1:6379> zrem salary xiaohong #移除有序集合中的key
(integer) 1
127.0.0.1:6379> zcard salary #获取有序集合中的个数
(integer) 2
redisTemplate
//集合中插入元素,并设置分数
redisTemplate.boundZSetOps("zSetKey").add("zSetVaule", 100D);
//集合中插入多个元素,并设置分数
DefaultTypedTuple<String> p1 = new DefaultTypedTuple<>("zSetVaule1", 2.1D);
DefaultTypedTuple<String> p2 = new DefaultTypedTuple<>("zSetVaule2", 3.3D);
redisTemplate.boundZSetOps("zSetKey").add(new HashSet<>(Arrays.asList(p1,p2)));
//打印全部元素
Set<String> range = redisTemplate.boundZSetOps("zSetKey").range(0, -1);
//获得指定元素的分数
Double score = redisTemplate.boundZSetOps("zSetKey").score("zSetVaule");
//返回集合元素在指定分数的排名
Set byScore = redisTemplate.boundZSetOps("zSetKey").rangeByScore(0D, 2.2D);
//返回集合内元素的排名,以及分数(从小到大)
Set<TypedTuple<String>> tuples = redisTemplate.boundZSetOps("zSetKey").rangeWithScores(0L, 3L);
for (TypedTuple<String> tuple : tuples) {
System.out.println(tuple.getValue() + " : " + tuple.getScore());
}
//从集合中删除元素
redisTemplate.boundZSetOps("zSetKey").remove("zSetVaule");
//删除指定索引范围内的元素
redisTemplate.boundZSetOps("zSetKey").removeRange(0L,3L);
//删除指定分数范围内的元素
redisTemplate.boundZSetOps("zSetKey").removeRangeByScorssse(0D,2.2D);
//返回指定成员的排名(从小到大)
Long startRank = redisTemplate.boundZSetOps("zSetKey").rank("zSetVaule");
//从大到小
Long endRank = redisTemplate.boundZSetOps("zSetKey").reverseRank("zSetVaule");
5.分布式锁(setnx)
127.0.0.1:6379> setnx mykey "redis"
(integer) 1
127.0.0.1:6379> keys *
1) "mykey"
127.0.0.1:6379> setnx mykey "mongdb" //不存在再设置(在分布式锁常用)如果mykey存在 创建失败
(integer) 0
127.0.0.1:6379> keys *
1) "mykey"
127.0.0.1:6379> get mykey
"redis"
存在问题:
- 业务超时解锁导致并发问题,业务执行时间超过锁超时时间
- redis主备切换临界点问题,主备切换后,A持有的锁还没有同步到新的主节点上,B在新主节点上获取锁
- redis集群脑裂,导致多个主节点,两个主都有锁
Redis注意事项
大Key:Value大于10KB就是大Key,使用大Key将导致Redis系统不稳定
消除大Key方法:
-
拆分:大Key拆小key
-
压缩:将value压缩后写入redis,读取时解压再使用,压缩算法gzip等
-
集合类结构hash,list,set
- 拆分:用hash取余
- 区分冷热:如榜单场景:只缓存前10页,后续数据走db
热Key:一个Key的QPS特别高,将导致Redis实例出现CPU负载突增,负责均衡流量不均的情况。导致单实例故障
解决:
- 在访问redis前,在业务服务侧设置Localcache,降低访问redis的QPS。如java 的Guava等
- 拆分:将key写成多份,但value是一样的,将qps分散到不同实例
- redis访问代理
慢查询:
- 大Key、热Key的读写;
- 一次操作过多的Key(mset/hmset/sadd/zadd)
导致缓存穿透、缓存雪崩的场景
- 缓存的内存有限时靠两个机制处理:数据过期和数据淘汰(缓存空间满了)
- 缓存雪崩是由于设置的过期时间相同,集中失效,将缓存失效时间随机打散
- 缓存击穿是缓存中没有,请求直接到达数据库,通过设置热点时间续期或者分布式锁来防止, 或者将冷数据加热进缓存并设置过期时间
- 缓存穿透是大量无效请求打进缓存和数据库中均没有,用布隆过滤器(验证有没有不存在的key)等解决,
- 在读多写少并发要求不高的时候采用事务+先更新数据库再更新缓存来达到一致性,
- 对于数据库更新+缓存删除策略,对于并发要求高,数据一致性要求好时采用先更新数据库,后删除缓存,并结合删除重试+补偿逻辑+缓存过期ttl等来处理