少了1秒

导航

 

1.项目中为什么要用Redis

Redis主要是用来解决性能和并发问题,若只是为了分布式锁这些其他功能,还有其他中间件 Zookpeer 等代替

性能:

如图所示: 

在执行耗时特别久,且结果不频繁变动的SQL的情况下,特别适合将运行结果放入缓存。这样,后面的请求就去缓存中读取,使请求可以迅速响应。

在秒杀系统中,同一时间,几乎所有人都在点,都在下单。。。执行的是同一操作——向数据库中查数据。

根据交互效果的不同,响应时间没有固定标准,在理想状态下,页面跳转需要在刹那间解决。

并发:

如图所示: 

如图所示,大并发的情况下,所有请求直接访问数据库,数据库会出现连接异常,需要Redis做一个缓冲操作,让请求先访问到Redis,而不是直接访问数据库

2.Redis 的数据类型及使用场景

在 Redis 中有五种数据类型

String:字符串
Hash:字典
List:列表
Set:集合
Sorted Set:有序集合

String:最常规的set/get操作,Value可以是String也可以是数字。一般组偶一些复杂的计数功能的缓存。

Hash:Value存放的是结构化的对象,比较方便的操作就是操作集中的某一字段。在做单点登录的时候,就是用这种数据结构存储用户信息,以CookieId作为Key,设置30分钟为缓存过期时间,很好的模拟出类似Session的效果

List: 使用List的数据结构可以做简单的消息队列的功能。可以利用lrange命令,做基于Redis的分页功能,性能极佳,用户体验好

Set: Set堆放的是是不重复值的集合,所以可以做全局去重。

在集群部署的环境中JVM自带的Set使用起来比较麻烦,利用交集,并集,差集等操作可计算共同喜好全部的喜好,自己独有的喜好。

Sorted Set: Sorted Set 多了一个权重参数Score,集合中的元素能够按照Score进行排列。可以做排行榜应用,取TOP N操作。Sorted Set可以用来做延时任务

3.Redis 内部结构

Redis 内部使用一个 redisObject 对象来表示所有的 key 和 value。

type :代表一个 value 对象具体是何种数据类型。 encoding :是不同数据类型在 redis 内部的存储方式,比如:type=string 代表 value 存储的是一个普通字符串,那么对应的 encoding 可以是 raw 或者是 int,如果是 int 则代表实际 redis 内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:”123” “456”这样的字符串。 vm 字段:只有打开了 Redis 的虚拟内存功能,此字段才会真正的分配内存,该功能默认是关闭状态的。 Redis 使用 redisObject 来表示所有的 key/value 数据是比较浪费内存的,当然这些内存管理成本的付出主要也是为了给 Redis 不同数据类型提供一个统一的管理接口,实际作者也提供了多种方法帮助我们尽量节省内存使用。

4.Redis 过期策略和内存淘汰机制

  • 定时删除:设置键的过期时间的同时,创造一个定时器(timer),让定时器在键的过期时间来临时,立即执行对键的删除操作。
  • 惰性删除:放任键过期不管,但每次从键空间获取键时,都检查键是否过期,如果过期的话,就删除该键;如果没有就返回该键
  • 定期删除:每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于删除多少过期键,以及检查多少数据库由算法决定。

例如Redis只能存5G数据,可是却写了10G,那会删除5G,怎么删的?

正解:Redis采用的是定期删除+惰性删除策略

Redis为什么不采用定时删除策略

定时删除需要一个定时器监视key,过期自动删除。虽然内存及时释放,但是十分消耗CPU资源。在大并发请求的情况下,CPU主要应用在处理请求,而不是删除key,因此没有采用这一策略

定期删除+惰性删除如何工作

定期删除,Redis默认每隔100ms检查,有过期key则删除。需要说明的是,Redis不是每隔100ms将所有key检查一次,而是随机抽取检查。如果只采用定期删除策略,会导致很多key到时间没有删除。于是需要惰性删除策略。

Redis 内存淘汰指的是用户存储的一些键被可以被 Redis 主动地从实例中删除,从而产生读 miss 的情况,那么 Redis 为什么要有这种功能?这就是我们需要探究的设计初衷。Redis 最常见的两种应用场景为缓存和持久存储,首先要明确的一个问题是内存淘汰策略更适合于那种场景?是持久存储还是缓存?

假设我们有一个 Redis 服务器,服务器物理内存大小为 1G 的,我们需要存在 Redis 中的数据量很小,这看起来似乎足够用很长时间了,随着业务量的增长,我们放在 Redis 里面的数据越来越多了,数据量大小似乎超过了 1G,但是应用还可以正常运行,这是因为操作系统的可见内存并不受物理内存限制,而是虚拟内存,物理内存不够用没关系,操作系统会从硬盘上划出一片空间用于构建虚拟内存,比如32位的操作系统的可见内存大小为 2^32,而用户空间的可见内存要小于 2^32 很多,大概是 3G 左右。好了,我们庆幸操作系统为我们做了这些,但是我们需要知道这背后的代价是不菲的,不合理的使用内存有可能发生频繁的 swap,频繁 swap 的代价是惨痛的。所以回过头来看,作为有追求的程序员,我们还是要小心翼翼地使用好每块内存,把用户代码能解决的问题尽量不要抛给操作系统解决。

内存的淘汰机制的初衷是为了更好地使用内存,用一定的缓存 miss 来换取内存的使用效率

作为 Redis 用户,我们如何使用 Redis 提供的这个特性呢?

# maxmemory-policy volatile-lru 

我们可以通过配置 redis.conf 中的 maxmemory 这个值来开启内存淘汰功能,至于这个值有什么意义,我们可以通过了解内存淘汰的过程来理解它的意义:

客户端发起了需要申请更多内存的命令(如set) Redis 检查内存使用情况,如果已使用的内存大于 maxmemory 则开始根据用户配置的不同淘汰策略来淘汰内存(key),从而换取一定的内存 如果上面都没问题,则这个命令执行成功 maxmemory 为 0 的时候表示我们对 Redis 的内存使用没有限制

内存淘汰只是 Redis 提供的一个功能,为了更好地实现这个功能,必须为不同的应用场景提供不同的策略,内存淘汰策略讲的是为实现内存淘汰我们具体怎么做,要解决的问题包括淘汰键空间如何选择?在键空间中淘汰键如何选择?

内存淘汰策略

Redis 提供了下面几种淘汰策略供用户选择,其中默认的策略为 noeviction 策略:

noeviction:当内存使用达到阈值的时候,所有引起申请内存的命令会报错

allkeys-lru:在主键空间中,优先移除最近未使用的key(最近使用较多)

volatile-lru:在设置了过期时间的键空间中,优先移除最近未使用的 key

allkeys-random:在主键空间中,随机移除某个 key

volatile-random:在设置了过期时间的键空间中,随机移除某个 key

volatile-ttl:在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除

这里补充一下主键空间和设置了过期时间的键空间,举个例子,假设我们有一批键存储在Redis中,则有那么一个哈希表用于存储这批键及其值,如果这批键中有一部分设置了过期时间,那么这批键还会被存储到另外一个哈希表中,这个哈希表中的值对应的是键被设置的过期时间。设置了过期时间的键空间为主键空间的子集。

5.聊聊 Redis 使用场景

  1. top 列表 产品运营总会让你展示最近、最热、点击率最高、活跃度最高等等条件的top list。很多更新较频繁的列表如果使用MC+MySQL维护的话缓存失效的可能性会比较大,鉴于占用内存较小的情况,使用Redis做存储也是相当不错的。

2.最后的访问

用户最近访问记录也是redis list的很好应用场景,lpush lpop自动过期老的登陆记录,对于开发来说还是非常友好的

3.手机验证码的,有效时间

4.限制用户登录的次数,比如一天错误登录次数10次。

5.投票系统 ,投票结果排序。 排行榜等等

6.存储社交信息,set的并集和交集。比较两个用户的共同粉丝

7.各种计数:商品维度计数(点赞数,评论数,浏览数)

8.发布订阅,聊天室等

6.Redis的持久化机制

Redis 有两种持久化机制:

RDB

RDB 持久化方式会在一个特定的间隔保存那个时间点的一个数据快照

AOF

AOF 持久化方式则会记录每一个服务器收到的写操作。在服务启动时,这些记录的操作会逐条执行从而重建出原来的数据。写操作命令记录的格式跟 Redis 协议一致,以追加的方式进行保存

Redis 的持久化是可以禁用的,就是说你可以让数据的生命周期只存在于服务器的运行时间里。两种方式的持久化是可以同时存在的,但是当 Redis 重启时,AOF 文件会被优先用于重建数据

7.Redis为什么是单线程的(单线程的Redis为什么这么快)

因为 CPU 不是 Redis 的瓶颈(CPU多核心数就是为了解决并发的)。Redis 的瓶颈最有可能是机器内存或者网络带宽。(以上主要来自官方 FAQ)既然单线程容易实现,而且 CPU 不会成为瓶颈,那就顺理成章地采用单线程的方案了。

  • 纯内存操作
  • 单线程操作,避免频繁的上下文切换
  • 采用了非阻塞I/O多路复用机制

8.缓存崩溃和缓存穿透问题

缓存雪崩

定义:

缓存雪崩是指原有缓存失效(过期),新缓存未到期期间,所有请求都去查询数据库,对数据库和CPU造成巨大压力,严重的话会造成数据库宕机,形成一系列连锁反应,造成整个系统崩溃。

解决策略: 碰到这种情况,一般并发量不是特别多的时候,使用最多的解决方案是

1.加锁排队:

public object GetProductListNew()
        {
            const int cacheTime = 30;
            const string cacheKey = "product_list";
            const string lockKey = cacheKey;
            
            var cacheValue = CacheHelper.Get(cacheKey);
            if (cacheValue != null)
            {
                return cacheValue;
            }
            else
            {
                lock (lockKey)
                {
                    cacheValue = CacheHelper.Get(cacheKey);
                    if (cacheValue != null)
                    {
                        return cacheValue;
                    }
                    else
                    {
                        cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。              
                        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
                    }                    
                }
                return cacheValue;
            }
        }

加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间 key 是锁着的,这时过来 1000 个请求 999 个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。

2.给每一个缓存数据增加相应的缓存标记,记录缓存是否失效,若缓存标记失效更新缓存

public object GetProductListNew()
        {
            const int cacheTime = 30;
            const string cacheKey = "product_list";
            //缓存标记。
            const string cacheSign = cacheKey + "_sign";
            
            var sign = CacheHelper.Get(cacheSign);
            //获取缓存值
            var cacheValue = CacheHelper.Get(cacheKey);
            if (sign != null)
            {
                return cacheValue; //未过期,直接返回。
            }
            else
            {
                CacheHelper.Add(cacheSign, "1", cacheTime);
                ThreadPool.QueueUserWorkItem((arg) =>
                {
                    cacheValue = GetProductListFromDB(); //这里一般是 sql查询数据。
                    CacheHelper.Add(cacheKey, cacheValue, cacheTime*2); //日期设缓存时间的2倍,用于脏读。                
                });
                
                return cacheValue;
            }
        }

缓存标记:记录缓存数据是否过期,如果过期会触发通知另外的线程在后台去更新实际key的缓存。

缓存数据:过期时间要比缓存标记的时间延长一倍,这样当缓存标记key过期以后,实际缓存还能把旧数据返回给调用端,直到另外的线程在后台更新完成后,才会返回新缓存

这种策略可以在一定程度上提高系统的吞吐量

缓存穿透

定义:

缓存穿透是指用户查询数据,在数据库没有,缓存中不会存在。导致用户在查询的时候在缓存中找不到,每次都要去数据库再查一遍,然后返回空。请求绕过缓存直接查数据库,也是经常提到的缓存命中率问题。 解决方案:

如果查询数据库也为空的话,直接设置一个默认值放在缓存中,这样第二次到缓存中获取就有值了,不会继续访问数据库了,简单粗暴解决问题

public object GetProductListNew()
        {
            const int cacheTime = 30;
            const string cacheKey = "product_list";

            var cacheValue = CacheHelper.Get(cacheKey);
            if (cacheValue != null)
                return cacheValue;
                
            cacheValue = CacheHelper.Get(cacheKey);
            if (cacheValue != null)
            {
                return cacheValue;
            }
            else
            {
                cacheValue = GetProductListFromDB(); //数据库查询不到,为空。
                
                if (cacheValue == null)
                {
                    cacheValue = string.Empty; //如果发现为空,设置个默认值,也缓存起来。                
                }
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
                
                return cacheValue;
            }
        }

9.Redis和数据库双写一致性问题

一致性问题分为最终一致性和强一致性。数据库和缓存双写,就必然会存在不一致的问题。前提是如果对数据有强一致性需求,不能放缓存。所以要解决的是最终一致性。

另外,现有的方案从根本上来说只能降低不一致发生的概率。通常的解决方案: 首先,采取正确的更新策略,先更新数据库,再删缓存。删缓存有可能存在删除缓存失败问题,需要提供一个补偿措施,例如通过消息队列实现。

10.如何解决 Redis 的并发竞争 Key 问题

这个问题大致就是同时多个子系统去Set一个key。需要注意什么呢,基本上都推荐使用Redis事务机制,但我并不推荐。因为生产环境中,基本都是Redis集群部署,做数据分片操作。一个事务中有涉及到多个key操作的时候,多个key不一定都存储在同一个redis-server上。因此,Redis的事务机制有些鸡肋。

如果这个key不要求顺序这种情况下可以准备一个分布式锁,大家去抢锁,抢到锁就做set操作,比较简单

如果对这个key操作要求顺序

假设有一个 key1,系统 A 需要将 key1 设置为 valueA,系统 B 需要将 key1 设置为 valueB,系统 C 需要将 key1 设置为valueC。

期望按照key1的值按照valueA>valueB>valueC,这种时候在数据保存到数据库的时候加一个时间戳。 假设时间戳如下:

系统 A key 1 {valueA 3:00}

系统 B key 1 {valueB 3:05}

系统 C key 1 {valueC 3:10}

假设B先抢到锁,将key1设置为{valueB 3:05}。接下来系统A抢到锁,发现自己的valueA时间戳早于B的,那么B就不做set操作了,以此类推,其他方法,比如利用队列,将set方法变成串行访问也可以。

posted on 2018-12-15 17:36  少了1s  阅读(125)  评论(0编辑  收藏  举报