redis详解1---相关的基础知识/数据类型/缓存的过期策略/双写一致性/内存存储和持久化

redis详解/redis相关的基础知识

摘要:本文是redis详解的第一部分,介绍redis相关的基础知识内存存储和持久化,redis作缓存时的注意要点,常见的数据类型缓存的过期策略,redis和数据库双写一致性的问题。参考的书籍有《redis实战 黄建宏译》《redis入门指南 李子骅》《高并发编程网》《尚硅谷的redis资料》,作为常见的nosql数据库,redis在项目中经常作为缓存来使用,更是面试中必备的技能

1、redis是什么?(远程字典服务器)

Redis是key-value形式的nosql数据库,内存中的数据结构存储系统,可以用作数据库、缓存和消息中间件。
为什么使用redis?

  • 在高并发场景下,如果需要经常连接结果变动频繁的数据库,会导致数据库读取及存取的速度变慢,数据库压力极大。因此我们需要通过缓存来减少数据库的压力,使得大量的访问进来能够命中缓存,只有少量的需求到数据库层。由于缓存基于内存,可支持的并发量远远大于基于硬盘的数据库。

传统关系型数据库和nosql区别?

  1. rdbms:组织结构化数据;结构化查询语言;数据和关系都存储在单独的表中;严格的一致性
  2. Nosql:代表的不仅仅是sql,没有声明新查询语言;键值对存储,最终一致性(非ACID),CAP定理

2、redis的应用

  1. 内存存储和持久化(持久化在下一篇文章中讲解)

我在项目中用到的:首页是系统的门户,也就是入口,所以首页的访问量是这个系统最大的。所以需要使用缓存来减轻读取数据库的压力。首页商品的广告位展示, redis保存的是hash类型的数据。 商品内容添加时,直接添加到数据库中;用户请求时,先调用服务层的服务区查询首页内容,然后到redis中查询,根据key查询,要是没有查询到,在从mysql数据库中查询内容分类信息,同时把查询结果写入redis中。(如何写:查询的是图书item对象,使用jsonutils工具类把对象转成json字符串
对首页内容修改时,先更新mysql数据库,然后调用服务层,清除redis相对应key的内容

  1. 取最新N个数据的操作,可以将最新的10条评论的ID放在redis的List集合里面
  2. 模拟类似于HttpSession这种需要设定过期时间的功能
  3. 发布、订阅消息系统
  4. 定时器、计数器

3、redis为什么可以做缓存/redis什么时候使用/保存的数据是什么/使用缓存的合理性问题?20181222****

  • 1、项目中使用redis的目的是什么?
    redis的所有的数据都是放到内存中,存取的速度非常快,减轻数据库的压力/提高存取的效率,在互联网项目中只要是涉及高并发或者是存在大量读数据的情况下都可以使用redis作为缓存。redis提供丰富的数据类型,除了缓存还可以根据实际的业务场景来决定redis的作用。例如使用redis保存用户的购物车信息生成订单号、访问量计数器、任务队列、排行榜等。

    Redis不仅能保存String类型的数据,还能保存Lists(有序)/Sets类型(无序) 的数据,而且还能完成排序(SORT) 等高级功能,在实现INCR,SETNX等功能的时候,保证了其操作的原子性,除此以外,还支持主从复制等功能

  • 2、Redis作查询缓存需要注意考虑以下几个问题 (防止脏读/序列化查询结果/为查询结果生成一个标识/怎么使用)

    (1)防止脏读
    我们缓存了查询结果,那么一旦数据库中的数据发生变化,缓存的结果就不可用了为了实现这一保证,可以在执行相关表的更新查询(update,delete,insert)查询前,让相关的缓存过期。这样下一次查询时程序会重新从数据库中读取新数据并缓存到redis中。
    Q: 在执行一条insert前我怎么知道应该让哪些缓存过期呢? **
    A: 对于Redis,我们可以使用Hash结构,让一张表对应一个Hash,所有在这张表上的查询都保存到该Hash下,这样当表数据发生变动时,直接设置过期即可。我们可以自定义一个注解,在数据库查询方法上通过注解的属性注明这个操作与哪些表相关,这样在执行过期操作时,就能直接从注解中得知应该让哪些Set过期了

    //表示该方法需要执行 (缓存是否命中? 返回缓存并阻止方法调用:执行方法并缓存结果)的缓存逻辑
    @RedisCache(type = JobPostModel.class)
    JobPostModel selectByPrimaryKey(Integer id);
    //表示该方法需要执行清除缓存逻辑
    @RedisEvict(type = JobPostModel.class)
    int deleteByPrimaryKey(Integer id);
    

    (2)序列化查询结果**
    利用JDK自带的ObjectInputStream/ObjectOutputStream将查询结果序列化成字节序列,即需要考虑Redis的实际存储问题(也可以使用第三方工具包jackson,json字符串化)
    (3)为查询结果生成一个标识
    被调用的方法所在的类名,被调用的方法的方法名,该方法的参数三者共同标识一条查询结果。也就是说,如果两次查询调用的类名、方法名和参数值相同,我们就可以确定这两次查询结果一定是相同的在数据没有变动的前提下)。因此,我们可以将这三个元素组合成一个字符串做为key,就解决了标识问题
    (4)以 AOP 方式使用Redis
    方法被调用之前,根据类名、方法名和参数值生成Key ;通过Key向Redis发起查询;如果缓存命中,则将缓存结果反序列化作为方法调用的返回值,并将其直接返回;如果缓存未命中,则继续向数据库中查询,并将查询结果序列化存入redis中,同时将查询结果返回

    例如,插入删除缓存逻辑如下:

    /* 在方法调用前清除缓存,然后调用业务方法
     * @param jp
     * @return
     * @throws Throwable */
    @Around("execution(* com.zjut.dao.mapper.JobPostModelMapper.insert*(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.update*(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.delete*(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.increase*(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.decrease*(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.complaint(..))" +
    			"|| execution(* com.zjut.dao.mapper.JobPostModelMapper.set*(..))")
    public Object evictCache(ProceedingJoinPoint jp) throws Throwable {}	
    

4、redis特征?

特征 详解
1、单线程 利用redis队列技术将访问变为串行访问,消除了传统数据库串行控制的开销
2、完全基于内存,存取速度快
3、redis的线程模型(做消息队列时会用到线程模型),使用了多路i/o复用模型 具体而言:多路复用I/O模型利用epoll(linux操作系统提供,select/poll的增强版)可以同时监视多个流的I/O事件,在空闲时,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,线程就会从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll只轮询哪些真正发出了事件的流),并且只依次顺序处理就绪的流,这种做法就避免了大量的无用操作。针对文件字节/字符操作时:使用的是面向缓冲区 的(传统上是使用面向流的)

5、redis内部结构?**

1、从使用者的角度 redis支持五种数据类型存储:1.string字符串2.hash散列3.list列表(排序的双向链表)4.set集合5.zset有序集合 //之后版本补充的数据类型有hyperloglog geo等
2、从内部实现的角度 ht(dict: 用于维护key和value映射关系的数据结构,与map和dictionary类似,在hash结构中,当他的filed较多时,便会采用dict来存储,redis配合使用dict和skiplist来共同维护一个sorted set) raw,embstr,intset,sds,ziplist,quicklist,skiplist(跳表)
3、为何这么设计? 存储效率,快速响应时间,单线程
4、dict详解? dict是一个基于hash表的算法,它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)rehash时每次只对一小部分进行rehash操作。

6、redis中各个数据类型的常用命令及使用场景?重点

数据类型 常用命令 实践
1、String(redis的string可以包含任何数据,比如jpg图片或者序列化的对象 value最多可以使512M) 赋值与取值 set/get ;递增数字 incr(存储的字符串是整型时可以使用)减少数字 decr 位操作:getbit:获取字符串指定位置的二进制位的值 实践:文章访问量统计,使用incr自增;setex(set with expire)键秒值 setnx(set if not exist) //setex:设置带过期时间的key,动态设置 setnx:只有在key不存在时才设置key的值;mset/mget/msetnx:mset同时设置一个或多个key-value对,mget:获取所有给定key的值;msetnx:当所有给定key不存在时,设置key-value对;getset:先get返回旧值,然后立即set
2、散列类型hash(是一个string类型的field和value的映射表,hash特别适用于存储对象) 1、假设命令都与String键相似,存在的意义?hash键可以将信息凝聚在一起,而不是直接分散存储在整个redis中,避免键名冲突;减少内存占用 //最重要的作用 2、什么时候不适合使用hash类型存储数据? 1、过期功能的使用,因为过期功能只能使用在key上;2、二进制操作命令,如setbit,getbit,bitop;3、需要考虑 数据量分布的问题 ,集群方案中,有分配槽的概念,使用crc16算法–>将结果分成%16384进行数据槽分配,对key进行crc16运算,得到的结果都会分配到同一个槽中,导致数据分配不均;命令: 赋值与取值hset/hget; 判断字段是否存在hexists 增加数组hincrby 实践:1、几亿用户系统的签到,去重登录次数统计,用户是否在线状态 setbit、getbit、bitcount命令,原理是:redis内构件一个足够长的数组,每个数组元素只能是0和1两个值 数组的下标index用来表示我们上面例子里面的用户id;2、hash实现幂等性请求,可以验证前端的重复请求,通过redis进行过滤;每次请求将request ip,参数,接口等hash作为key存储redis,然后设置有效期,下次请求过来时先在redis中,检索有没有这个key,进而验证是不是一定时间内过来的重复提交。
3、列表类型list(按照插入顺序排序,可以添加元素到列表的头部或尾部) 命令:增加元素 lpush/rpush 从哪个列表两端弹出元素 lpop/rpop(可以使用list类型模拟栈lpush+lpop和队列lpush+rpop的操作)获取列表片段 lrange 删除 lrem 实践:1、消息队列 2、阻塞队列
4、集合类型set,特点:无序,不允许重复 命令:增加删除 sadd/srem 获取集合中的所有元素 smembers 判断元素是否在集合中 sismember 集合间运算差集sdiff sinter交集 sunion并集从集合中弹出一个元素 spop 获取集合中元素个数 scard 进行集合运算并将结果存储 sdiffstore 随机获取集合中的元素 srandmember 实践:1、可能认识的人(实现关注模型) A{1,2,3}B{2,3,4}c{4,6,7} 共同关注的人->交集 可能认识的人->差集 ;2、实现电商商品的筛选,入库时会遍历他的静态标签列表,品牌,尺寸,处理器,内存 ->交集;3、基于集合键运算,实现人和支付系统的对账, 订单系统产生的数据,支付系统产生的数据 -》差集;4、基于集合键,实现直播刷礼物,转发微博等抽奖活动,sadd key(userId)//刷礼物,转发微博加到集合中,smembers key;//获取所有用户,spop key【count】 /srandmember key【count】//抽取count个中奖者;5、基于集合键,实现点赞,签到,like等功能,sadd //点赞,srem //取消点赞,sismember //检查用户是否点过赞,smembers //获取点赞用户列表;6、基于集合键,实现Tag功能 豆瓣,sadd 添加标签,srem 移除标签
5、有序集合类型zset,特点:底层使用跳跃表做了分值字段 且分值越大,集合的下标越小 时间复杂度为0(n) 命令:增加 zadd, 获取元素的分数 zscore key member 获取排名在某个范围的元素列表 zrange{分数相同则比较字符串 时间复杂度为0(logn+m)} 计算有序集合的交集 zinterstore 实践:1、基于有序集合键,实现自动补齐功能 ,这与用户使用量有关,先对用户的输入做分词处理 后面使用zinterstore计算交集 调用ajax来查找;2、实现单日排行榜(今日热搜 搜索热点)zadd/zincr/zrevrange;3、实现周,月,年的排行榜,在单日热点的基础上做交集运算 zunionstore zrevrange

7、redis数据淘汰策略(LRU算法,slab槽分配),如何减少内存碎片 (面试官的问题的解决方案)

  • 1、redis的过期策略和内存淘汰机制。redis存的数据超出内存值,是如何删除过多的数据的?
    redis采用的是定期删除+惰性删除策略。定时器ttl负责监视key,过期则自动删除。虽然内存及时释放,但是十分消耗cpu资源。定期删除+惰性删除工作机制–定期:redis默认每隔100ms检查,有过期的key则删除。redis不是每隔100ms将所有的key检查一次,而是随机抽取进行检查。如果只是采取定时删除策略,会导致很多key到时间没有删除。

  • 2、存在的问题:
    如果定时删除没有删除掉key,并且也没及时请求key,也就是惰性删除也没生效,这样redis的内存会越来越高,这时:应该采用内存淘汰机制

  • 3、内存淘汰方案
    redis.conf中有一行配置:

    #maxmemory-policy 
    volatile-lru(在设置了过期时间的key中,移除最近最少使用的key  不推荐)
    volatile-ttl  //从已设置过期时间的数据集中挑选将要过期的数据淘汰
    volatile-random  //从已设置过期时间的数据集中任意选择数据淘汰
    allkeys-lru(内存不足以容纳新的写入数据时,在键操作中,移除最近最少使用的key)**推荐**
    allkeys-random(内存不足以容纳新的写入数据时,在键操作中,随机移除某个key)
    noeviction(内存不足以容纳新的写入数据时,新写入操作会报错)
    

8、redis和数据库双写一致性的问题? **我的项目中没有双写的问题,因为redis只是用作缓存,哈哈 20181104

1、对于一致性要求高的场景,实现同步方案,即查询redis,若查询不到再从DB查询,保存到redis;更新redis时,先更新数据库,然后将redis内容设置为过期(不要去更新缓存内容,直接设置缓存过期),再用ZINCRBY增量修正redis数据;
2、并发程度高的,采用异步队列的方式,采用kafka等消息中间件处理消息生产和消费;
3、阿里的同步工具canal,实现方式是模拟mysql slave和master的同步机制,监控DB binlog的日志更新来触发redis的更新,解放程序员的双手,减少工作量
4、利用mysql触发器API进行编程,c/c++语言是基础,学习成本高

redis新数据定时同步到数据库过程:
1、定时任务定时同步redis与数据库的数据,数据库里存储着演示数据,通过数据库的数据与redis对比,得出需要更新的数据;
2、在更新过程中,redis的数据还在增长,a、需要先读redis的数据,记下时间;b、再查询指定时间段里的数据库的数据;c、再用ZINCRBY增量修正redis数据,而不是直接用哪个ZADD覆盖redis数据

一致性分为强一致性和最终一致性:数据库和缓存双写,就必然会存在不一致的问题。如果对数据有强一致性的要求,就不能放缓存。使用了redis,只能保证最终一致性。采取正确的更新策略,先更新数据库,再删缓存,例如:对商品库存进行操作时,先更新数据库中的信息,然后删除缓存;其次:因为可能存在删除缓存失败的问题,提供一个补偿措施即可,例如利用消息队列。

不同情况下的常用处理方法

  • 1、简单逻辑处理:

    1、增 db–cache:数据库插入成功,添加到缓存
    2、删:cache–》db:先删缓存,再删数据库
    3、改:db–》cache:先改数据库,再改缓存
    4、查:cache–>db–>cache:先查缓存,没有查数据库,然后添加进缓存

  • 2、复杂逻辑处理:

    一个service中有多次数据库交互,并且由于spring事务传播性不同有时只在最外层事务提交时提交(spring默认事务传播性)
    service(事务)–service/dao----cache
    增:db:只插入数据库
    删:cache–>db:先删缓存,再删数据库
    改:cache–>db:先删缓存,再删数据库
    查:cache–>db–>cache:先查缓存,没有则查库添加到缓存


9、redis键的过期时间

作用:清除缓存数据 可以按小时(用户的登录信息),按天(商品详情页面的缓存),为键设置过期时间,过期后没自动删除该键;对于hash表这种数据类型,只能为整个键(field)设置过期时间,而不能为键里面的单个元素设置过期时间.


10、redis数据的同步问题。更改了后台的数据(比如修改价格),而此数据在redis中保存着。 被新浪问到过

用 ActiveMQ 的消息传递来进行解耦。当我们在后台进行了商品数据的更新后,就利用 MQ 发送一个消息出去,比如说消息内容是商品 id,然后在缓存模块中监听该消息,拿到商品id后到缓存中删除该商品数据就行了。

前(redis)后(mysql)台数据的同步
方案1:读:读缓存redis,没有的话,读mysql,并将mysql的值写入到redis;写:写mysql,成功后,再删除redis中对应的缓存。
方案2使用RDB快照技术+AOF增量技术(redis变更的数据)。同步对redis的写操作命令到日志文件,然后同步到mysql数据库中。 最终一致性


11、解析配置文件redis.conf (端口号6379)

includes里面包括
daemonize 设置为yes可以启动守护线程 此时redis把pid写入var/run/redis.pid中
tcp-keepalive syslog-enabled是否把日志输出到syslog中
snapshotting快照配置 save写操作次数 RDB是整个内存的压缩过的snapshot,RDB的数据结构,可以配置复合的快照触发机制
limits限制 可以设置内存淘汰机制
append only mode追加配置(always everysec:每秒同步一次 no:等操作系统进行数据缓存同步到磁盘)
vm-enabled no 指定是否启动虚拟内存机制 默认为no,即VM机制将数据分页存放,由redis将访问量较少的页(冷数据)swap到磁盘上,访问多后会从磁盘换出到内存


13、Redis 为什么是单线程的?(后期补充)
14、redis的pfadd,pfcount?(后期补充)

诸葛亮说过:“夫君子之行,静以修身,俭以养德,非淡泊无以明志,非宁静无以致远。”历史上成功的人,大多都是经历了一番苦痛与孤独,你们需要努力,更需要坚持,更要耐得住寂寞,要坚信自己心中的信念,加油

你可能感兴趣的:(深入理解数据库)