8

Redis详解 - 忧愁的chafry

 1 year ago
source link: https://www.cnblogs.com/chafry/p/16740130.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Redis介绍

    1.Redis 是一个基于内存的高性能 key-value 数据库。是完全开源免费的,用C语言编写的,遵守BSD协议

    2.Redis 特点:

      1)Redis 是基于内存操作的,吞吐量非常高,可以在 1s内完成十万次读写操作
      2)Redis 的读写模块是单线程,每个操作都具原子性
      3)Redis 支持数据的持久化,可以将内存中的数据保存在磁盘中,重启可以再次加载,但可能会有极短时间内数据丢失
      4)Redis 支持多种数据结构,String,list,set,zset,hash等

针对五种数据结构的介绍【针对场景部分,有些并不适用,但是用于拓展视野】

    1.字符串String(用的多)

      1)常用操作

        【1】单值操作

SET key value //存入字符串键值对
GET key //获取一个字符串键值
SETNX key value //存入一个不存在的字符串键值对,有点类似原子操作,如果没有才给存入,有则失败
DEL key [key ...] //删除一个键
EXPIRE key seconds //设置一个键的过期时间(秒)

        【2】批量操作

MSET key value [key value ...] //批量存储字符串键值对
MGET key [key ...] //批量获取字符串键值

        【3】原子操作

INCR key //将key中储存的数字值加1
DECR key //将key中储存的数字值减1
INCRBY key increment //将key所储存的值加上increment
DECRBY key decrement //将key所储存的值减去decrement

      2)应用场景

        【1】单值缓存

SET  key  value     
GET  key    

        【2】对象缓存

//这两种情况要区分,你对这个对象的操作是整体多还是属性值多,因为java中使用的话对象的数据类型需要序列化【存储和取出都要】
//而且分开存的话有助于在不同地方使用不同属性值,但是却要取出整个对象的局面。(虽然分开存消耗内存更多,但是从传输角度来想,有可能消耗更小,但都是分场景的)
1)  SET  user:1  value(json格式数据)
2)  MSET  user:1:name  zhuge   user:1:balance  1888  //针对对象的值分开存储的批量操作
    MGET  user:1:name   user:1:balance 

        【3】分布式锁实现(下面仅仅是示例,这个实现其实要考虑的问题很多,可查看 Redis高并发分布式锁详解

SETNX  product:10001  true      //返回1代表获取锁成功
SETNX  product:10001  true      //返回0代表获取锁失败
。。。执行业务操作
DEL  product:10001          //执行完业务释放锁

SET product:10001 true  ex  10  nx  //防止程序意外终止导致死锁

        【4】计数器

INCR article:readcount:{文章id}   
GET article:readcount:{文章id} 

        【5】实现分布式session共享(可查看 分布式Session的实现详解 )

spring session + redis实现session共享

        【6】分布式全局ID 

INCRBY  orderId  1000       //redis批量生成序列号提升性能

    2.哈希hash

      1)常用操作

        【1】单值操作

HSET  key  field  value             //存储一个哈希表key的键值
HGET  key  field                //获取哈希表key对应的field键值
HSETNX  key  field  value       //存储一个不存在的哈希表key的键值

        【2】批量操作

HMSET  key  field  value [field value ...]  //在一个哈希表key中存储多个键值对
HMGET  key  field  [field ...]      //批量获取哈希表key中多个field键值
HDEL  key  field  [field ...]       //删除哈希表key中的field键值
HLEN  key               //返回哈希表key中field的数量
HGETALL  key                //返回哈希表key中所有的键值

        【3】原子操作

HINCRBY  key  field  increment      //为哈希表key中field键的值加上增量increment

      2)应用场景

        【1】对象缓存(由于redis设置过期时间只针对顶级key类型,而不支持对hash类型内部,故塞得多了容易造成bigKey问题)

HMSET  user  {userId}:name  zhuge  {userId}:balance  1888
HMSET  user  1:name  zhuge  1:balance  1888
HMGET  user  1:name  1:balance  

        【2】电商购物车

1)以用户id为key   //以用户ID为key,避免多个用户存储在一个hash里面(避免bigKey)//针对未登录的可以构建虚拟ID,对登录时的数据进行合并
2)商品id为field
3)商品数量为value
4)可以针对key设置过期时间  //设置过期时间可以在不用的时候,redis自己回收

购物车操作
hset cart:1001 10088 1  //添加商品
hincrby cart:1001 10088 1  //增加数量
hlen cart:1001  //商品总数 
hdel cart:1001 10088  //删除商品
hgetall cart:1001  //获取购物车所有商品

      3)优缺点

优点
    1)同类数据归类整合储存,方便数据管理
    2)相比string操作消耗内存与cpu更小
    3)相比string储存更节省空间

缺点
    1)过期功能不能使用在field上,只能用在key上。//(这也是容易造成bigKey问题的本质,设置过期时间是为了让redis自己去回收,设置不了就只能靠自己去回收,不回收容易造成内存挤爆,也容易出现阻塞请求的情况)
    2)Redis集群架构下不适合大规模使用。//(因为构建集群的本质是平摊请求和数据,提高处理量和扛并发,如果hash的值会被存储在某个节点中,如果值很大,那么容易出现请求倾斜,那么这个结点容易被打挂)

    3.列表list(用的多)

      1)常用操作

LPUSH  key  value [value ...]       //将一个或多个值value插入到key列表的表头(最左边)
RPUSH  key  value [value ...]       //将一个或多个值value插入到key列表的表尾(最右边)
LPOP  key           //移除并返回key列表的头元素
RPOP  key           //移除并返回key列表的尾元素
LRANGE  key  start  stop        //返回列表key中指定区间内的元素,区间以偏移量start和stop指定
BLPOP  key  [key ...]  timeout  //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待                  timeout秒,如果timeout=0,一直阻塞等待
BRPOP  key  [key ...]  timeout  //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待                  timeout秒,如果timeout=0,一直阻塞等待

      2)应用场景

        【1】常用数据结构(应对分布式下某些数据结构的要求)

Stack(栈) = LPUSH + LPOP
Queue(队列)= LPUSH + RPOP
Blocking MQ(阻塞队列)= LPUSH + BRPOP

        【2】微博消息和微信公号消息(这种更多体现在即时通讯软件上面)

你关注了A,B等大V
1)A发微博,消息ID为10018
LPUSH  msg:{你-ID}  10018
2)B发微博,消息ID为10086
LPUSH  msg:{你-ID} 10086
3)查看最新微博消息
LRANGE  msg:{你-ID}  0  4

    4.集合set(比较重要)

      1)常用操作

SADD  key  member  [member ...]         //往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM  key  member  [member ...]         //从集合key中删除元素
SMEMBERS  key                   //获取集合key中所有元素
SCARD  key                  //获取集合key的元素个数
SISMEMBER  key  member          //判断member元素是否存在于集合key中
SRANDMEMBER  key  [count]           //从集合key中随机选出count个元素,元素不从key中删除
SPOP  key  [count]              //从集合key中随机选出count个元素,元素从key中删除

      2)运算操作

SINTER  key  [key ...]              //交集运算,多个集合共有的元素的集合
SINTERSTORE  destination  key  [key ..]     //将交集结果存入新集合destination中
SUNION  key  [key ..]               //并集运算,将元素汇总成一个集合
SUNIONSTORE  destination  key  [key ...]        //将并集结果存入新集合destination中
SDIFF  key  [key ...]               //差集运算,相当于第一个集合减去后面多个集合的并集
SDIFFSTORE  destination  key  [key ...]     //将差集结果存入新集合destination中

      3)应用场景

        【1】微信抽奖小程序

1)点击参与抽奖加入集合
SADD key {userlD}
2)查看参与抽奖所有用户
SMEMBERS key      
3)抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]

        【2】微信微博点赞,收藏,标签

1) 点赞
SADD  like:{消息ID}  {用户ID}
2) 取消点赞
SREM like:{消息ID}  {用户ID}
3) 检查用户是否点过赞
SISMEMBER  like:{消息ID}  {用户ID}
4) 获取点赞的用户列表
SMEMBERS like:{消息ID}
5) 获取点赞用户数 
SCARD like:{消息ID}

        【3】集合操作实现微博微信关注模型

1) A关注的人: 
ASet-> {C, D}
2) B关注的人:
BSet--> {A, E, C, D}
3) C关注的人: 
CSet-> {A, B, E, D, F)

//重点通过A与关注他的B【但A没有关注B】
4) A,B共同关注: 
SINTER ASet BSet--> {C, D}
5) A关注的人也关注他(B): 
SISMEMBER CSet B 
SISMEMBER DSet B
6) A可能认识的人: 
SDIFF BSet ASet->{A, E}

        【4】集合操作实现电商商品筛选

SADD  brand:huawei  P40
SADD  brand:xiaomi  mi-10
SADD  brand:iPhone iphone12
SADD os:android  P40  mi-10
SADD cpu:brand:intel  P40  mi-10
SADD ram:8G  P40  mi-10  iphone12

SINTER  os:android  cpu:brand:intel  ram:8G   {P40,mi-10}

    5.有序集合zset

      1)常用操作

ZADD key score member [[score member]…] //往有序集合key中加入带分值元素
ZREM key member [member …]      //从有序集合key中删除元素
ZSCORE key member           //返回有序集合key中元素member的分值
ZINCRBY key increment member        //为有序集合key中元素member的分值加上increment 
ZCARD key               //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES]  //正序获取有序集合key从start下标到stop下标的元素,WITHSCORES参数的作用:就是查询结果带上分数
ZREVRANGE key start stop [WITHSCORES]   //倒序获取有序集合key从start下标到stop下标的元素 

      2)集合操作

ZUNIONSTORE destkey numkeys key [key ...]   //并集计算
ZINTERSTORE destkey numkeys key [key ...]   //交集计算

      3)应用场景

        【1】Zset集合操作实现排行榜

1)点击新闻
ZINCRBY  hotNews:20190819  1  守护香港ID  //针对单条数据,集合名,浏览次数,文章ID
2)展示当日排行前十
ZREVRANGE  hotNews:20190819  0  9  WITHSCORES 
3)七日搜索榜单计算
ZUNIONSTORE  hotNews:20190813-20190819  7  hotNews:20190813  hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819  0  9  WITHSCORES

针对Redis的探索

  1)Redis是单线程吗?

    1.Redis并不是真正意义上的单线程,Redis的单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。

    2.但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。

  2)Redis 单线程为什么还能这么快?

    1.因为它所有的数据都在内存中,所有的运算都是内存级别的运算,而且单线程避免了多线程的切换性能损耗问题。正因为 Redis 是单线程,所以要小心使用 Redis 指令,对于那些耗时的指令(比如keys),一定要谨慎使用,一不小心就可能会导致 Redis 卡顿。

      【1】keys:全量遍历键,用来列出所有满足特定正则字符串规则的key,当redis数据量比较大时,性能比较差,要避免使用

keys  *   //展示全部key值
keys  ab*c  //展示全部符合正则匹配的key值

      【2】scan:渐进式遍历键。(常用这种替代keys指令)

SCAN cursor [MATCH pattern] [COUNT count]
示例:SCAN 0  MATCH  test*key  COUNT  100

        说明:scan 参数提供了三个参数,第一个是 cursor 整数值(hash桶的索引值),第二个是 key 的正则模式,第三个是一次遍历的key的数量(参考值,底层遍历的数量不一定),并不是符合条件的结果数量。第一次遍历时,cursor 值为 0,然后将返回结果中第一个整数值作为下一次遍历的 cursor。一直遍历到返回的 cursor 值为 0 时结束。

        注意:但是scan并非完美无瑕, 如果在scan的过程中如果有键的变化(增加、 删除、 修改) ,那么遍历效果可能会碰到如下问题: 新增的键可能没有遍历到, 遍历出了重复的键等情况, 也就是说scan并不能保证完整的遍历出来所有的键, 这些是我们在开发时需要考虑的。

        示例展示:

             

2168218-20220930030848128-1886305455.png

        示例分析:   

          这个和底层实现有关,Redis的底层相当于一个HashMap(将数据散列存储到key中存储一样),scan 每次去遍历的时候会去遍历其中存储数据的一个key值,一次最多拿三个,并且返回下次的游标,便于下次获取。

          如果一共有36个数据分别散落于4个key为key1,key2,key3,key4中,其中key1有8个数据,key2有7个,key3有10个,key4有11个。那么先去扫描key1的槽,

第三次拿只会拿到2个数据,然后key1槽扫描完就不会再扫描了,而会去扫描key2的槽。按此逻辑走完全部的槽。这也是为什么我们需要在开发的时候注意的。

  3)Redis 单线程如何处理那么多的并发客户端连接?

    1.Redis的IO多路复用:redis利用epoll来实现IO多路复用,将连接信息和事件放到队列中,依次放到文件事件分派器,事件分派器将事件分发给事件处理器。

    2.连接数是存在限制的:

# 查看redis支持的最大连接数,在redis.conf文件中可修改,# maxclients 10000
127.0.0.1:6379> CONFIG GET maxclients
    ##1) "maxclients"
    ##2) "10000"

  4)Redis 真的可以在 1s内完成十万次读写操作吗?

    其实不是的,这个与当前的服务器有关,大多数应该是几万。Redis 自带了一个叫 redis-benchmark 的工具(放在Redis的src目录下)来模拟 N 个客户端同时发出 M 个请求:

[root@node1 bin]# redis-benchmark --help
Usage: redis-benchmark [-h <host>] [-p <port>] [-c <clients>] [-n <requests]> [-k <boolean>]

 -h <hostname>          指定服务器主机名 (默认 127.0.0.1)
 -p <port>              指定服务器端口 (默认 6379)
 -s <socket>            指定服务器 socket
 -a <password>          指定redis密码
 -c <clients>           指定并发连接数 (默认 50)
 -n <requests>          指定请求数 (默认 100000)
 -d <size>              SET/GET 命令的值bytes单位 默认是2
 --dbnum <db>           指定redis的某个数据库,默认是0数据库
 -k <boolean>           指定是否保持连接 1是保持连接 0是重新连接,默认为 1
 -r <keyspacelen>       指定get/set的随机值的范围。
 -P <numreq>            管道请求测试,默认0没有管道测试
 -e                     如果有错误,输出到标准输出上。
 -q                     静默模式,只显示query/秒的值
 --csv                  指定输出结果到csv文件中
 -l                     生成循环,永久执行测试
 -t <tests>             仅运行以逗号分隔的测试命令列表

理解Redis对Lua脚本的操作

    1)Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:

      【1】减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。与管道类似。

      【2】原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。

      【3】替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,官方推荐如果要使用redis的事务功能可以用redis lua替代。

  2.简单使用

    1)从Redis2.6.0版本开始,通过内置的Lua解释器,可以使用EVAL命令对Lua脚本进行求值。EVAL命令的格式如下:

格式:EVAL script numkeys key [key ...] arg [arg ...]
示例:eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second

      示例结果【展示了如何进行传参】

           

2168218-20220930073101422-1066994569.png

        【1】script参数是一段Lua脚本程序,它会被运行在Redis服务器上下文中,这段脚本不必定义为一个Lua函数。numkeys参数用于指定键名参数的个数。

        【2】键名参数 key [key ...] 从EVAL的第三个参数开始算起,表示在脚本中所用到的那些Redis键(key),这些键名参数可以在 Lua中通过全局变量KEYS数组,用1为基址的形式访问( KEYS[1] , KEYS[2] ,以此类推)。

        【3】在命令的最后,那些不是键名参数的附加参数 arg [arg ...] ,可以在Lua中通过全局变量ARGV数组访问,访问的形式和KEYS变量类似( ARGV[1] 、 ARGV[2] ,诸如此类)。

        【4】其中 "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 是被求值的Lua脚本,数字2指定了键名参数的数量, key1和key2是键名参数,分别使用 KEYS[1] 和 KEYS[2] 访问,而最后的 first 和 second 则是附加参数,可以通过 ARGV[1] 和 ARGV[2] 访问它们。

    2)在 Lua 脚本中,可以使用redis.call()函数来执行Redis命令(使用Jedis调用):

jedis.set("product_stock_10016", "15");  //初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
                " local a = tonumber(count) " +
                " local b = tonumber(ARGV[1]) " +
                " if a >= b then " +
                "   redis.call('set', KEYS[1], a-b) " +
                "   return 1 " +
                " end " +
                " return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);

    注意:不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。  

Redis缓存设计中存在的问题

  1.缓存穿透

    1)说明:

      【1】缓存穿透是指查询一个根本不存在的数据, 缓存层和存储层都不会命中, 通常出于容错的考虑, 如果从存储层查不到数据则不写入缓存层。

      【2】缓存穿透将导致不存在的数据每次请求都要到存储层去查询, 失去了缓存保护后端存储的意义。

      【3】造成缓存穿透的基本原因有两个:

          第一, 自身业务代码或者数据出现问题。

          第二, 一些恶意攻击、 爬虫等造成大量空命中。

    2)处理:

      【1】缓存空对象

//主体逻辑
product = productDao.get(productId);
if (product != null) {
    redisUtil.set(productCacheKey, JSON.toJSONString(product),genProductCacheTimeout(), TimeUnit.SECONDS);
} else {
    redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
}

//空缓存的过期时间获取,这个时间不宜过大,一分钟左右即可,时间过大容易缓存大量空数据,消耗Redis存储资源。
//时间过小容易很快过期,但是我们可以进行读延期,你一直查,我们一直延期,你不查了,过一段时间就会失效
private Integer genEmptyCacheTimeout() {
    return 60 + new Random().nextInt(30);
}

      【2】布隆过滤器

          1.布隆过滤器介绍

            1)对于恶意攻击,向服务器请求大量不存在的数据造成的缓存穿透,还可以用布隆过滤器先做一次过滤,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往后端发送。当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

            2)布隆过滤器就是一个大型的位数组和几个不一样的无偏 hash 函数。所谓无偏就是能够把元素的 hash 值算得比较均匀。

            3)向布隆过滤器中添加 key 时,会使用多个 hash 函数对 key 进行 hash 算得一个整数索引值然后对位数组长度进行取模运算得到一个位置,每个 hash 函数都会算得一个不同的位置。再把位数组的这几个位置都置为 1 就完成了 add 操作。

            4)向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在,因为这些位被置为 1 可能是因为其它的 key 存在所致。如果这个位数组长度比较大,存在概率就会很大,如果这个位数组长度比较小,存在概率就会降低。

            5)这种方法适用于数据命中不高、 数据相对固定、 实时性低(通常是数据集较大) 的应用场景, 代码维护较为复杂, 但是缓存空间占用很少。

               

2168218-20221003043718843-790116555.png

          3.用redisson实现布隆过滤器

            1)引入依赖:

<dependency>
   <groupId>org.redisson</groupId>
   <artifactId>redisson</artifactId>
   <version>3.6.5</version>
</dependency>

            2)示例伪代码:

public class RedissonBloomFilter {

    public static void main(String[] args) {
        Config config = new Config();
        config.useSingleServer().setAddress("redis://localhost:6379");
        //构造Redisson
        RedissonClient redisson = Redisson.create(config);

        RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
        //初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
        bloomFilter.tryInit(100000000L,0.03);
        //将zhuge插入到布隆过滤器中
        bloomFilter.add("abc");

        //判断下面号码是否在布隆过滤器中
        System.out.println(bloomFilter.contains("bcd"));//false
        System.out.println(bloomFilter.contains("cde"));//false
        System.out.println(bloomFilter.contains("abc"));//true
    }
}

            3)使用布隆过滤器需要把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放,布隆过滤器缓存过滤伪代码:

//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
        
//把所有数据存入布隆过滤器
void init(){
    for (String key: keys) {
        bloomFilter.put(key);
    }
}

String get(String key) {
    // 从布隆过滤器这一级缓存判断下key是否存在
    Boolean exist = bloomFilter.contains(key);
    if(!exist){
        return "";
    }
    // 从缓存中获取数据
    String cacheValue = cache.get(key);
    // 缓存为空
    if (StringUtils.isBlank(cacheValue)) {
        // 从存储中获取
        String storageValue = storage.get(key);
        cache.set(key, storageValue);
        // 如果存储数据为空, 需要设置一个过期时间(300秒)
        if (storageValue == null) {
            cache.expire(key, 60 * 5);
        }
        return storageValue;
    } else {
        // 缓存非空
        return cacheValue;
    }
}

          3.注意:布隆过滤器不能删除数据,如果要删除得重新初始化数据。

  2.缓存失效(击穿)

    1)说明:由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉,对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。造成的原因是:我们为了便捷,提供了批量生产与批量修改操作,那么容易出现设置的过期时间一直问题。

    2)处理:

      【1】同一获取过期时间的入口,针对获取过期时间采取添加随机时间错开时间段。

public static final Integer PRODUCT_CACHE_TIMEOUT = 60 * 60 * 24; //设置为1天或者更少,这种一般要考虑凌晨时期的空窗期(没人使用)
private Integer genProductCacheTimeout() {
    //这对过期时间添加随机时间
    return PRODUCT_CACHE_TIMEOUT + new Random().nextInt(5) * 60 * 60; 
}

      【2】对于获取数据步骤,要对缓存增加读延期

public static final String EMPTY_CACHE = "{}";
private Product getProductFromCache(String productCacheKey) {

    String productStr = redisUtil.get(productCacheKey);
    if (!StringUtils.isEmpty(productStr)) {
        if (EMPTY_CACHE.equals(productStr)) {
            redisUtil.expire(productCacheKey, genEmptyCacheTimeout(), TimeUnit.SECONDS);  //读延期,延长过期时间
            return null;
        }
        product = JSON.parseObject(productStr, Product.class);
        redisUtil.expire(productCacheKey, genProductCacheTimeout(), TimeUnit.SECONDS); //读延期,延长过期时间
    }
    return product;
}

  3.缓存雪崩

    1)介绍:

      【1】缓存雪崩指的是缓存层支撑不住或宕掉后, 流量直接打向后端存储层。

      【2】由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务(比如超大并发过来,缓存层支撑不住,或者由于缓存设计不好,类似大量请求访问bigkey,导致缓存能支撑的并发急剧下降), 于是大量请求都会打到存储层, 存储层的调用量会暴增, 造成存储层也会级联宕机的情况。

      【3】总的来说,就是Redis配置中存在 maxclients  10000 属性值限制(设置能连上redis的最大客户端连接数量。默认是10000个客户端连接。由于redis不区分连接是客户端连接还是内部打开文件或者和slave连接等,所以maxclients最小建议设置到32。如果超过了maxclients,redis会给新的连接发送’max number of clients reached’,并关闭连接。)

    2)处理:

      【1】保证缓存层服务高可用性,比如使用Redis Sentinel或Redis Cluster。(即使用集群增加雪崩的上限,增加雪崩的难度)

      【2】依赖隔离组件为后端限流熔断并降级。比如使用Sentinel或Hystrix限流降级组件(最好结合 redis-benchmark 的工具,压测部署在服务器上集群能抗住多少并发)。比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。

      【3】提前演练。 在项目上线前, 演练缓存层宕掉后, 应用以及后端的负载情况以及可能出现的问题, 在此基础上做一些预案设定。

  4.热点缓存key重建优化

    1)介绍:

      【1】使用“缓存+过期时间”的策略既可以加速数据读写, 又保证数据的定期更新, 这种模式基本能够满足绝大部分需求。 但是有两个问题如果同时出现, 可能就会对应用造成致命的危害:

        1.当前key是一个热点key(例如一个热门的娱乐新闻),并发量非常大。

        2.重建缓存不能在短时间完成, 可能是一个复杂计算, 例如复杂的SQL、 多次IO、 多个依赖等。

      【2】在缓存失效的瞬间, 有大量线程来重建缓存, 造成后端负载加大, 甚至可能会让应用崩溃。

    2)处理(主要就是要避免大量线程同时重建缓存):

      【1】利用互斥锁来解决,此方法只允许一个线程重建缓存, 其他线程等待重建缓存的线程执行完, 重新从缓存获取数据即可。

      【2】采用DCL【双重检查锁(double-checked locking)】,可以在完成重建后加快返回速度。

      【3】代码展示:

product = getProductFromCache(productCacheKey);
if (product != null) {
    return product;
}
//DCL
RLock hotCacheLock = redisson.getLock(LOCK_PRODUCT_HOT_CACHE_PREFIX + productId);
hotCacheLock.lock();
try {
    product = getProductFromCache(productCacheKey);
    if (product != null) {
        return product;
    }

    product = productDao.get(productId);
    if (product != null) {
        redisUtil.set(productCacheKey, JSON.toJSONString(product),genProductCacheTimeout(), TimeUnit.SECONDS);
    } else {
        redisUtil.set(productCacheKey, EMPTY_CACHE, genEmptyCacheTimeout(), TimeUnit.SECONDS);
    }

} finally {
    hotCacheLock.unlock();
}
return product;

  5.缓存与数据库双写不一致 

    1)介绍:在大并发下,同时操作数据库与缓存会存在数据不一致性问题【这种采用双删是解决不了的】

      【1】双写不一致情况

               

2168218-20221002174308654-1416083374.png

      【2】读写并发不一致

             

2168218-20221002174345447-1666368000.png

    2)处理:

      【1】对于并发几率很小的数据(如个人维度的订单数据、用户数据等),这种几乎不用考虑这个问题,很少会发生缓存不一致,可以给缓存数据加上过期时间,每隔一段时间触发读的主动更新即可。

      【2】就算并发很高,如果业务上能容忍短时间的缓存数据不一致(如商品名称,商品分类菜单等),缓存加上过期时间依然可以解决大部分业务对于缓存的要求。

      【3】如果不能容忍缓存数据不一致,可以通过加分布式读写锁保证并发读写或写写的时候按顺序排好队,读读的时候相当于无锁。

      【4】也可以用阿里开源的canal通过监听数据库的binlog日志及时的去修改缓存,但是引入了新的中间件,增加了系统的复杂度。

    【1】以上都是针对读多写少的情况加入缓存提高性能,如果写多读多的情况又不能容忍缓存数据不一致,那就没必要加缓存了,可以直接操作数据库。

    【2】如果数据库抗不住压力,还可以把缓存作为数据读写的主存储,异步将数据同步到数据库,数据库只是作为数据的备份。

    【3】放入缓存的数据应该是对实时性、一致性要求不是很高的数据。

    【4】不要为了用缓存,同时又要保证绝对的一致性做大量的过度设计和控制,增加系统复杂性!(如延迟双删加上休眠时间,这就很不可取)

查看redis服务运行信息

   1.Info:查看redis服务运行信息,分为 9 大块,每个块都有非常多的参数,这 9 个块分别是: 

    1.Server 服务器运行的环境参数 

    2.Clients 客户端相关信息 

    3.Memory 服务器运行内存统计数据 

    4.Persistence 持久化信息 

    5.Stats 通用统计数据 

    6.Replication 主从复制相关信息 

    7.CPU CPU 使用情况 

    8.Cluster 集群信息 

    9.KeySpace 键值对统计数量信息

          2168218-20221003045352886-1765789534.png

  3.核心参数说明

connected_clients:2                  # 正在连接的客户端数量

instantaneous_ops_per_sec:789        # 每秒执行多少次指令

used_memory:929864                   # Redis分配的内存总量(byte),包含redis进程内部的开销和数据占用的内存
used_memory_human:908.07K            # Redis分配的内存总量(Kb,human会展示出单位)
used_memory_rss_human:2.28M          # 向操作系统申请的内存大小(Mb)(这个值一般是大于used_memory的,因为Redis的内存分配策略会产生内存碎片)
used_memory_peak:929864              # redis的内存消耗峰值(byte)
used_memory_peak_human:908.07K       # redis的内存消耗峰值(KB)

maxmemory:0                         # 配置中设置的最大可使用内存值(byte),默认0,不限制,一般配置为机器物理内存的百分之七八十,需要留一部分给操作系统
maxmemory_human:0B                  # 配置中设置的最大可使用内存值
maxmemory_policy:noeviction         # 当达到maxmemory时的淘汰策略

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK