一些概念回顾

缓存穿透

概念:

缓存没有数据,而且数据库也没有数据。

​ 当这种情况大量出现或被恶意攻击时,接口的访问全部透过Redis访问数据库,而数据库中也没有这些数据,我们称这种现象为"缓存穿透"。缓存穿透会穿透Redis的保护,提升底层数据库的负载压力,同时这类穿透查询没有数据返回也造成了网络和计算资源的浪费。

一般解决方案

  • 如果Redis内不存在该数据,则通过布隆过滤器判断数据是否在底层数据库内;
  • 如果布隆过滤器告诉我们该key在底层库内不存在,则直接返回null给客户端即可,避免了查询底层数据库的动作;
  • 如果布隆过滤器告诉我们该key极有可能在底层数据库内存在,那么将查询下推到底层数据库即可
布隆过滤器有误判率,虽然不能完全避免数据穿透的现象,但已经可以将绝大部份的的穿透查询给屏蔽在Redis层了,极大的降低了底层数据库的压力,减少了资源浪费。

缓冲击穿

概念

  • 一般是指缓存没有但是数据库有的数据。
  • 比如热点数据失效,导致大量合法热点请求打向数据库,此时数据库压力山大

一般解决方案:

  • 延长热点key的过期时间或者设置永不过期,如排行榜,首页等一定会有高并发的接口;
  • 利用互斥锁保证同一时刻只有一个客户端可以查询底层数据库的这个数据,一旦查到数据就缓存至Redis内,避免其他大量请求同时穿过Redis访问底层数据库。

缓存雪崩

概念

缓存雪崩时缓存击穿的加强版,大量的key几乎同时过期,然后大量并发查询穿过redis击打到底层数据库上,此时数据库层的负载压力会骤增,我们称这种现象为"缓存雪崩"。

一般解决方案

缓存预热

​ 缓存预热如字面意思,当系统上线时,缓存内还没有数据,如果直接提供给用户使用,每个请求都会穿过缓存去访问底层数据库,

如果并发大的话,很有可能在上线当天就会宕机,因此我们需要在上线前先将数据库内的热点数据缓存至Redis内再提供出去使用,

这种操作就成为"缓存预热"。缓存预热的实现方式有很多,比较通用的方式是写个批任务,在启动项目时或定时去触发将底层数据库内的热点数据加载到缓存内。

缓存更新

​ 缓存服务(Redis)和数据服务(底层数据库)是相互独立且异构的系统,在更新缓存或更新数据的时候无法做到原子性的同时更新两边的数据,因此在并发读写或第二步操作异常时会遇到各种数据不一致的问题。如何解决并发场景下更新操作的双写一致是缓存系统的一个重要知识点。

第二步操作异常:缓存和数据的操作顺序中,第二个动作报错。如数据库被更新, 此时失效缓存的时候出错,缓存内数据仍是旧版本;

缓存更新的设计模式有四种:

Cache aside查询:先查缓存,缓存没有就查数据库,然后加载至缓存内;更新:先更新数据库,然后让缓存失效;或者先失效缓存然后更新数据库;

Read through:在查询操作中更新缓存,即当缓存失效时,Cache Aside 模式是由调用方负责把数据加载入缓存,而 Read Through 则用缓存服务自己来加载;

Write through:在更新数据时发生。当有数据更新的时候,如果没有命中缓存,直接更新数据库,然后返回。如果命中了缓存,则更新缓存,然后由缓存自己更新数据库;

Write behind caching:俗称write back,在更新数据的时候,只更新缓存,不更新数据库,缓存会异步地定时批量更新数据库;

Cache aside:

  • 为了避免在并发场景下,多个请求同时更新同一个缓存导致脏数据,因此不能直接更新缓存而是另缓存失效

  • 先更新数据库后失效缓存:并发场景下,推荐使用延迟失效(写请求完成后给缓存设置1s过期时间),在读请求缓存数据时若redis内已有该数据(其他写请求还未结束)则不更新。当redis内没有该数据的时候(其他写请求已另该缓存失效),读请求才会更新redis内的数据。这里的读请求缓存数据可以加上失效时间,以防第二步操作异常导致的不一致情况。

  • 先失效缓存后更新数据库:并发场景下,推荐使用延迟失效(写请求开始前给缓存设置1s过期时间),在写请求失效缓存时设置一个1s延迟时间,然后再去更新数据库的数据,此时其他读请求仍然可以读到缓存内的数据,当数据库端更新完成后,缓存内的数据已失效,之后的读请求会将数据库端最新的数据加载至缓存内保证缓存和数据库端数据一致性;在这种方案下,第二步操作异常不会引起数据不一致,例如设置了缓存1s后失效,然后在更新数据库时报错,即使缓存失效,之后的读请求仍然会把更新前的数据重新加载到缓存内。

  • 推荐使用先失效缓存,后更新数据库,配合延迟失效来更新缓存的模式;
    

    四种缓存更新模式的优缺点

    • Cache Aside:实现起来较简单,但需要维护两个数据存储,一个是缓存(Cache),一个是数据库(Repository);
    • Read/Write Through:只需要维护一个数据存储(缓存),但是实现起来要复杂一些;
    • Write Behind Caching:与Read/Write Through 类似,区别是Write Behind Caching的数据持久化操作是异步的,但是Read/Write Through 更新模式的数据持久化操作是同步的。优点是直接操作内存速度快,多次操作可以合并持久化到数据库。缺点是数据可能会丢失,例如系统断电等。
    缓存本身就是通过牺牲强一致性来提高性能,因此使用缓存提升性能,就会有数据更新的延迟性。这就需要我们在评估需求和设计阶段根据实际场景去做权衡了。
    

缓存降级

​ 缓存降级是指当访问量剧增、服务出现问题(如响应时间慢或不响应)或非核心服务影响到核心流程的性能时,即使是有损部分其他服务,仍然需要保证主服务可用。可以将其他次要服务的数据进行缓存降级,从而提升主服务的稳定性。

降级的目的是保证核心服务可用,即使是有损的。如去年双十一的时候淘宝购物车无法修改地址只能使用默认地址,这个服务就是被降级了,这里阿里保证了订单可以正常提交和付款,但修改地址的服务可以在服务器压力降低,并发量相对减少的时候再恢复。

降级可以根据实时的监控数据进行自动降级也可以配置开关人工降级。是否需要降级,哪些服务需要降级,在什么情况下再降级,取决于大家对于系统功能的取舍。

CAP

CAP定理是加州大学的计算机科学家 Eric Brewer 在 1998年提出,也就是下面的3个英文单词, 原始的CAP理论有非常精准的定义,讨论的范围其实也有非常有局限性,现在大家讨论的CAP其实跟原始的有不少地方是不一致,下面讨论的也是被大家讨论的一些内容。

CAP更多的是关注数据,而不是整个系统,实际项目中往往要处理各种数据,有些数据要求更多的一致性,有些数据要求更多的可用性,如果武断的说要CP还是AP往往是比较难的。

  • Consistency 一致性 (读写数据的一致性)

    • 简单理解就是多个数据实例的数据要保证一致,这里又可以用很多个粒度,比如强一致性,最终一致性等等

    • 实际数据同步时候往往不能忽略网络延迟,相同机房可能几毫秒同步完成,远距离的跨机房可能要几十毫秒甚至更多,所以有些非常关键的数据要求必须一致的时候往往把P扔了,只能单点写入,其它节点做备份。

  • Availability 可用性(服务高可用性)

    • 客户端发送一个请求,必须给出正确回应,所以说起高可用,往往不是单点的,是多节点的服务,因为单点如果出现故障则整个系统挂掉,根本没法向外提供服务 对应上面的图来说 也就是往往有G1 G2 往往还会有Gn等多个实例提供服务
  • Partition tolerance 分区容错性

    • 分布式系统,正常情况下多个节点之间是可以互通的,但不是讨论的范围,P讨论的如果出现故障,节点之间不能互通,所以整个网络就被分成了多块区域,而数据就是分散在这些不能连通的区域中,这个称为分区。容错的意思就是分区了(不能互相连通)也要能给提供服务,让客户端正常访问,不能出现单点故障。
  • 为什么要P非要在C和A之间做取舍

    • 拿上面的G1 G2例子来说,如果必须A:要求必须实现一致性,则网络不通G2就要停止对外提供服务

      因为要C,G1改为v1 G2也必须改为v1,但是如果说P 也就是G1不能给G2发消息的情况(网络不通),G2无法实现数据同步而保持一致性,所以这个时候就要让G2停止对外提供服务,因为数据不一致了,不能对外提供服务。

      如果G2不能对外提供服务,此时G1挂掉了,就没有节点能给对外提供服务了,也就是没有高可用了 也就是没有了A

    • 反过来说也是一样的 如果就是要A,也就是不要一致性了,必须保证服务高可用,G1挂掉了 G2数据虽然数据不是最新的(存在数据不一致),也必须让G2对外提供服务,所以这个时候就是舍弃了C(放弃数据一致性)

  • 实际项目往往不是非常粗暴的舍弃A或者C,这里的舍弃往往只是非常短时间的舍弃。因为讨论舍弃C/A的前提是P出错了,也就是节点之间不能互通了,分区出现故障了,这个时间往往是比较短暂的,等分区故障恢复后往往就可以CA了。另外针对不同的数据往往也会选择不同的策略。

    比如一般用户账号数据(id 密码)往往是要求一致性更多点,但其它一些信息数据(昵称、兴趣)等可能要求可用性更多些

ACID

ACID 是数据库管理系统为了保证事务的正确性而提出来的一个理论,ACID 包含四个约束,下面我来解释一下。

Atomicity(原子性)

一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。

Consistency(一致性)

在事务开始之前和事务结束以后,数据库的完整性没有被破坏。

Isolation(隔离性

数据库允许多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。事务隔离分为不同级别,包括读未提交(Read uncommitted)、读提交(read committed)、可重复读(repeatable read)和串行化(Serializable)。

Durability(持久性)

事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。

可以看到,ACID 中的 A(Atomicity)和 CAP 中的 A(Availability)意义完全不同,而 ACID 中的 C 和 CAP 中的 C 名称虽然都是一致性,但含义也完全不一样。ACID 中的 C 是指数据库的数据完整性,而 CAP 中的 C 是指分布式节点中的数据一致性。再结合 ACID 的应用场景是数据库事务,CAP 关注的是分布式系统数据读写这个差异点来看,其实 CAP 和 ACID 的对比就类似关公战秦琼,虽然关公和秦琼都是武将,但其实没有太多可比性。

BASE

BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。

基本可用(Basically Available)

分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。

这里的关键词是“部分”和“核心”,具体选择哪些作为可以损失的业务,哪些是必须保证的业务,是一项有挑战的工作。例如,对于一个用户管理系统来说,“登录”是核心功能,而“注册”可以算作非核心功能。因为未注册的用户本来就还没有使用系统的业务,注册不了最多就是流失一部分用户,而且这部分用户数量较少。如果用户已经注册但无法登录,那就意味用户无法使用系统。例如,充了钱的游戏不能玩了、云存储不能用了……这些会对用户造成较大损失,而且登录用户数量远远大于新注册用户,影响范围更大。

软状态(Soft State)

允许系统存在中间状态,而该中间状态不会影响系统整体可用性。这里的中间状态就是 CAP 理论中的数据不一致。

最终一致性(Eventual Consistency)

系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。

这里的关键词是“一定时间” 和 “最终”,“一定时间”和数据的特性是强关联的,不同的数据能够容忍的不一致时间是不同的。举一个微博系统的例子,用户账号数据最好能在 1 分钟内就达到一致状态。因为用户在 A 节点注册或者登录后,1 分钟内不太可能立刻切换到另外一个节点,但 10 分钟后可能就重新登录到另外一个节点了,而用户发布的最新微博,可以容忍 30 分钟内达到一致状态。因为对于用户来说,看不到某个明星发布的最新微博,用户是无感知的,会认为明星没有发布微博。“最终”的含义就是不管多长时间,最终还是要达到一致性的状态。

BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充。前面在剖析 CAP 理论时,提到了其实和 BASE 相关的两点:

  • CAP 理论是忽略延时的,而实际应用中延时是无法避免的。

这一点就意味着完美的 CP 场景是不存在的,即使是几毫秒的数据复制延迟,在这几毫秒时间间隔内,系统是不符合 CP 要求的。因此 CAP 中的 CP 方案,实际上也是实现了最终一致性,只是“一定时间”是指几毫秒而已。

  • AP 方案中牺牲一致性只是指分区期间,而不是永远放弃一致性。

这一点其实就是 BASE 理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性

分布式锁

Mysql锁

方式

​ 使用数据库的唯一索引特性,获取锁就是插入一条记录(唯一索引保证只能插入一次),释放锁就是删除这条记录

优点

​ 使用非常简单 开发难度很低

缺点

  • 锁没有失效时间 如果应用拿到锁之后挂掉,导致锁无法正常释放,那么其它应用就再也拿不到锁,业务无法开展。

  • 没有拿到锁的应用需要不停的检查锁是否已经释放(记录是否存在),这样比较浪费cpu资源,也会给数据库压力

  • 如果搞一个监控程序监视锁,控制超时时间,超过时间就强制删除锁,一来这里的超时时间不好控制,而来监控程序也可能挂掉 还是存在锁无法释放的风险。

Redis分布式锁

方式1__setnx key value

> setnx lock:codehole true             //使用setnx(set if not exists) 指令。在redis里面占坑 谁占到了就是抢锁成功
OK
... do something critical ...
> del lock:codehole										//使用del 命令释放锁
(integer) 1
------------------------问题--------------------
1. 中间的处理逻辑如果出现异常,比如程序死掉了,会导致无法调用del命令,也就是锁无法释放
2. 应用可以强制抢锁,因为谁都可以上来直接del 这本身是不安全了
3. 锁释放了 其它应用不知道,所以只能不停的查询锁释放还在,浪费资源

方式2__set key value ex timeout nx

> set lock:codehole true ex 5 nx   
//setnx升级版 加上了设置超时时间 而且redis支持这2个功能为原子操作 过了一定时间应用不释放 redis强制释放锁
... do something critical ...
> del lock:codehole
------------------------问题--------------------
1. 超时时间不好设置 如果过长 会导致一旦持有锁的应用挂掉了 很长一段时间 业务无法继续
2. 超时时间太短 业务逻辑还没有执行完成,锁就被redis强制释放,其它应用就会重新持有这把锁,这样就没法保证临界区的代码严格串行
	---比如秒杀商品超买问题
3. 应用可以强制抢锁,因为谁都可以上来直接del 这本身是不安全了(应用超时后业务逻辑处理完毕 也会误删锁 下面有例子)
4. 锁释放了 其它应用不知道,所以只能不停的查询锁释放还在,浪费资源
时间线 线程1 线程2 线程3
时刻1 执行 setnx mylock val1 加锁 执行 setnx mylock val2 加锁 执行 setnx mylock val2 加锁
时刻2 加锁成功 加锁失败 加锁失败
时刻3 执行任务… 尝试加锁… 尝试加锁…
时刻4 任务继续(锁超时,自动释放了) setnx 获得了锁(因为线程1的锁超时释放了) 仍然尝试加锁…
时刻5 任务完毕,del mylock 释放锁 执行任务中… 获得了锁(因为线程1释放了线程2的)

方式3__set lockKey randomNum ex timeout nx 删除的时候匹配随机数 其它应用误删锁

大致逻辑如下:
1. 线程1 准备释放锁 , 锁的key 为 mylock  锁的 value 为 thread1_magic_num
2. 查询当前锁 current_value = get mylock
3. 判断    if current_value == thread1_magic_num -- > 是  我(线程1)的锁
          else                                   -- >不是 我(线程1)的锁
4. 是我的锁就释放,否则不能释放(而是执行自己的其他逻辑)。  
  • 因为上述的逻辑直接使用redis命令无法做到原子性 所以直接使用比较危险 可以用lua脚本实现(redis执行lua脚本是原子的 要么都成功 要么都失败)
if redis.call('get', lockKey) == randomNum加锁时候的随机数
    then 
	    return redis.call('del', lockKey) 
	else 
	    return 0 
end
  • 问题:
    • 超时时间还是不好设置的问题,还是无法解决逻辑执行时间长,导致锁超时被redis强制释放,其它线程抢锁成功,同时执行临界区代码的问题

方式4: redlock

​ 上述3种方式都有一个共同的问题 就是如果redis是单点的,不是集群的,一旦redis节点挂点就不能够对外提供服务了。

如果是集群的话,例如redis sentinel集群中,我们具有多台redis,他们之间有着主从的关系,例如一主二从,set命令对应的数据写到主库,然后同步到从库。当我们申请一个锁的时候,对应就是一条命令 setnx mykey myvalue ,在redis sentinel集群中,这条命令先是落到了主库。假设这时主库down了,而这条数据还没来得及同步到从库,sentinel将从库中的一台选举为主库了。这时,我们的新主库中并没有mykey这条数据,若此时另外一个client执行 setnx mykey hisvalue , 也会成功,即也能得到锁。这就意味着,此时有两个client获得了锁。虽然这个情况发生的记录很小,只会在主从failover的时候才会发生,大多数情况下、大多数系统都可以容忍,但是不是所有的系统都能容忍这种瑕疵。

原理:

为了解决故障转移情况下的缺陷,Antirez 发明了 Redlock 算法,使用redlock算法,需要多个redis实例,加锁的时候,它会想多半节点发送 setex mykey myvalue 命令,只要过半节点成功了,那么就算加锁成功了。释放锁的时候需要想所有节点发送del命令。这是一种基于【大多数都同意】的一种机制。感兴趣的可以查询相关资料。在实际工作中使用的时候,我们可以选择已有的开源实现,python有redlock-py,java 中有Redisson redlock。

redlock确实解决了上面所说的“不靠谱的情况”。但是,它解决问题的同时,也带来了代价。你需要多个redis实例,你需要引入新的库 代码也得调整,性能上也会有下降。

总结

​ 总体来讲方式3用的比较多,相对比较安全,但要根据实际业务场景适当设置超时时间,过长过短都不好。

​ 应用一定要捕捉异常,如果出现异常一定要释放锁。

​ 如果因为业务逻辑执行过长,redis超时强制删除了锁,自己释放锁的时候发现不是自己的锁了,也要根据实际情况做相应的处理,比如回滚之类的。

Zookeeper分布式锁

Zookeeper节点类型

  • 永久性节点(有序和无序)

    不会因为会话结束或者超时而消失,

    有序的话 会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,依次类推

  • 临时性节点(有序和无序)

    如果会话结束或者超时就会消失。

Zookeeper有节点变化通知其它客户端的特性

  • 客户端可以针对某一个zk节点(目录 但可以有数据)设置监控事件(可以是节点变更、删除等等)和对应处理方法
  • 当对应的zk节点发生变化 会通知客户端进行影响处理

利用临时有序节点和事件通知特性实现分布式锁

  1. 创建一个锁目录 /lock; 在 /lock 下创建临时的且有序的子节点

  2. Client1对应的子节点为/lock/lock-0000000000,Clinet2为/lock/lock-0000000001 …..

  3. 每个client都查询目录下的所有节点列表 判断自己释放为数字最小的节点

    3.1 是则加锁成功 进行业务处理,处理完毕 删除对应lock下的子节点

    3.2 不是最小节点 则监听自己的前一个节点 等待前一个节点处理完毕后通知自己

  4. 如果持有锁的节点死掉了 会话超时临时节点会被zk删除 也就是释放了锁

redis

5种数据类型

​ 内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:type代表一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式,比如:type=string代表value存储的是一个普通字符串,那么对应的encoding可以是raw或者是int,如果是int则代表实际redis内部是按数值型类存储和表示这个字符串的,当然前提是这个字符串本身可以用数值表示,比如:“123” “456"这样的字符串。

string 字符串类型

最基本的数据类型,可以保存字符串、数字、二进制,本身是二进制安全的。

常用操作命令

保存为字符串操作相关:

set key value  #设置字符串key的值为value
append key value  #在原先字符串后面追加
strlen key   #查看key这个字符串的长度
setrange key offset value  #Overwrite part of a string at key starting at the specified offset 改变字符串某一位的值
getrange key start end 	#Get a substring of the string stored at a key 获取子串
setnx  key value 如果不存在则设置kv 存在不设置
SET key value [expiration EX seconds|PX milliseconds] [NX|XX]  #设置kv 并且设置超时时间 往往用在分布式锁 NX表示如果存在则不能设置 不覆盖
mset mget一次设置多个kv  一次获取多个kv

保存为数字的操作相关

incr key   # Increment the integer value of a key by one
INCRBY key increment   #Increment the integer value of a key by the given amount
DECR key   #Decrement the integer value of a key by one
DECRBY key decrement  # Decrement the integer value of a key by the given number

保存为二进制位的相关操作

setbit key offset value   #Sets or clears the bit at offset in the string value stored at key
		#offset如果超过本身长度 会动态的扩 非offset的位置都是0 如果要get的话 可能出现乱码 
BITCOUNT key [start end]  #Count set bits in a string  这里的start end指的是字节 不是每个自己里面的offset
BITOP operation destkey key [key ...]  #位操作
127.0.0.1:6379> setbit k1 1 1     
127.0.0.1:6379> setbit k1 7 1
127.0.0.1:6379> get k1       k1的二进制位 0100 0001
"A"
127.0.0.1:6379> setbit k2 1 1
127.0.0.1:6379> setbit k2 6 1
127.0.0.1:6379> get k2       k1的2的二进制位 0100 0010
"B"
想执行 k1&k2 
127.0.0.1:6379> bitop and result k1 k2
(integer) 1
127.0.0.1:6379> get result
"@"                         result是@ 对应二进制位 0100 0000

bitop and|or|xor|not

应用场景

  • session共享
  • kv缓冲
  • 计数器:微博数、粉丝数
  • 小文件系统 key:文件名 value: 具体数据
  • bitmap的一些使用场景:
    • 布隆过滤器
    • 记录一个人一年的登录情况 可以用365个位表示 哪一天登陆了就设置为1 等等
    • 统计活跃用户数之类的 每天一个key 记录每一天每个用户登陆请求(映射到每一位) 然后每一天的key通过或运算

list

​ redis实现的是双向循环链表 所以非常容易实现分布式队列或者分布式栈,也就是字符串链表,插入本身是有顺序的,第一个插入,取出第一个还是这个元素。

常用操作命令

LPUSH key value [value ...] # Prepend one or multiple values to a list 每次都从左边头部压入链表 可以一次压入多个元素
LPOP key                    # Remove and get the first element in a list 从左边出弹出一个元素
对应的右端操作就是 RPUSH RPOP 只是每次都从右端操作
LRANGE key start stop  #Get a range of elements from a list  获取某一个区间的元素列表 最右端支持为下标-1的下标计数
LTRIM key start stop   #Trim a list to the specified range 删除 【start,stop】左边和右边的元素

应用场景

  • 分布式队列和栈

  • 将redis用作日志容器器 多个client将日志写入redis list,搞一个进程读取list写入磁盘

  • 取最新的N个数据操作 比如最新的10条评论 最新登陆的10个用户id列表 超过这个范围的从数据库获取

    • /把当前登录人添加到链表里
      ret = r.lpush("login:last_login_times", uid)
      //保持链表只有N位
      ret = redis.ltrim("login:last_login_times", 0, N-1)
      //获得前N个最新登陆的用户Id列表
      last_login_list = r.lrange("login:last_login_times", 0, N-1)
      
  • 分布式任务分发器

    • 多个任务派发进程 将单个任务请求写入redis list
    • 多个任务处理进程 从list中pop取任务处理

hash

​ 存放的value本身是是个hashmap 也就是一个string 类型的 field 和 value 的映射表

​ 对数据的修改和存取都可以直接通过其内部Map的Key(Redis里称内部Map的key为field), 也就是通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题。

​ 实现方式:上面已经说到Redis Hash对应Value内部实际就是一个HashMap,实际这里会有2种不同实现,这个Hash的成员比较少时Redis为了节省内存会采用类似一维数组的方式来紧凑存储,而不会采用真正的HashMap结构,对应的value redisObject的encoding为zipmap,当成员数量增大时会自动转成真正的HashMap,此时encoding为ht

常用操作命令

127.0.0.1:6379> hset infoid name hkt age 20   #key:infoid 对应value为map[name:hkt,age:20]
HSET key field value					#这里的infoid可以理解成c++语言的map名字 field理解为map的key value就是map[key]的值
127.0.0.1:6379> hget infoid name   #获取infoid这个hashmap对应key为name的值  string name=infoid[name]
"hkt"
127.0.0.1:6379> hget infoid age   #获取infoid这个hashmap对应key为age的值  int age=infoid[age]
"20"
127.0.0.1:6379> hset infoid age 30 #修改infoid这个hashmap对应key为age的值 infoid[age]=30
127.0.0.1:6379> hget infoid age
"30"
127.0.0.1:6379> hgetall infoid     #获取infoid这个hashmap所有的key和value 现实项目不常用 因为比较耗时
1) "name"
2) "hkt"
3) "age"
4) "30"
127.0.0.1:6379> hkeys infoid  #获取infoid这个hashmap对应的所有key
1) "name"
2) "age"
127.0.0.1:6379> hvals infoid #获取infoid这个hashmap对应的所有value
1) "hkt"
2) "30"

应用场景

  • 聚集一些相关的数据做缓存(频繁访问不怎么修改的,但彼此有相关 比如用户信息 粉丝 关注等等)
  • 商品详情页的多个tab页面也可以缓存到hash中

set

​ 集合、无序(插入也是没有顺序)、没有重复 ,底层就是hashmap 只是value为null,可以支持数学上的交集、并集、查集操作

常用命令

127.0.0.1:6379> sadd set1 a b c d   添加元素 a b c d到集合set1
(integer) 4
127.0.0.1:6379> sadd set2 b c d e
(integer) 4
127.0.0.1:6379> scard set1  #获取set1集合数量
(integer) 4
127.0.0.1:6379> smembers set1  #获取集合set1中所有的成员
1) "b"
2) "a"
3) "d"
4) "c"
127.0.0.1:6379> sinter set1 set2  #取2个集合的交集
1) "b"
2) "d"
3) "c"
127.0.0.1:6379> sdiff set1 set2  #取2个集合的差集 set1有 set2没有
1) "a"
127.0.0.1:6379> sdiff set2 set1  #取2个集合的差集 set2有 set1没有
1) "e"
127.0.0.1:6379> sunion set1 set2 #取2个集合的并集  不保存到redis
1) "b"
2) "a"
3) "d"
4) "c"
5) "e"
127.0.0.1:6379> sunionstore set3 set1 set2  #取2个集合的并集  保存到redis
(integer) 5
127.0.0.1:6379> smembers set3
1) "b"
2) "a"
3) "d"
4) "c"
5) "e"
127.0.0.1:6379> sismember set1 a  #是否在某个集合当中
(integer) 1
127.0.0.1:6379> srandmember set3  #随机返回集合中的某一个元素
"c"
127.0.0.1:6379> srandmember set3 3 #随机返回集合中的多个元素 如果大于集合总个数 也只能返回集合总个数
1) "b"
2) "a"
3) "e"
127.0.0.1:6379> srandmember set3 -7 #随机返回集合中的多个元素 如果大于集合个数 会出现重复 但个数是你所要求的
1) "a"
2) "b"
3) "a"
4) "a"
5) "c"
6) "e"
7) "e"
127.0.0.1:6379> spop set1   #随机删除一个元素
"d"
127.0.0.1:6379> srem set1 b #指定删除某一个元素
(integer) 1

应用场景

  • 分布式去重 向redis某个set里面不断扔数据就可以了
  • 分布式并查集计算
  • 案例:在微博中,可以将一个用户所有的关注人存在一个集合中,将其所有粉丝存在一个集合。Redis还为集合提供了求交集、并集、差集等操作,可以非常方便的实现如共同关注、共同喜好、二度好友等功能,对上面的所有集合操作,你还可以使用不同的命令选择将结果返回给客户端还是存集到一个新的集合中
  • 随机抽奖 随机验证码等等

zset

​ 集合、不重复、有序,sorted set可以通过用户额外提供一个优先级(score)的参数来为成员排序,如果需要一个有序的不重复的集合可以选择zset。和Set相比,Sorted Set关联了一个double类型权重参数score,使得集合中的元素能够按score进行有序排列,redis正是通过分数来为集合中的成员进行从小到大的排序。zset的成员是唯一的,但分数(score)却可以重复

​ 实现方式:Redis sorted set的内部使用HashMap和跳跃表(SkipList)来保证数据的存储和有序,HashMap里放的是成员到score的映射,而跳跃表里存放的是所有的成员,排序依据是HashMap里存的score,使用跳跃表的结构可以获得比较高的查找效率,并且在实现上比较简单

常用命令

127.0.0.1:6379> zadd zset 100 hkt 99 wxl 98 hyx  #往zset有序集合添加3个内容 hkt 100分 wxl 99分 hyx 98分
(integer) 3
127.0.0.1:6379> zrange zset 0 -1  #列出全部元素 也可以加上withscores就会把分数返回
1) "hyx"
2) "wxl"
3) "hkt"
127.0.0.1:6379> zscore zset hkt   #获取某个成员的分数
"200"
ZREM key member    #删除某个成员
ZREVRANGE key start stop  # with scores ordered from high to low  也就是将序 默认是升序
127.0.0.1:6379> zpopmin zset   #弹出最小的元素 
1) "hyx"
2) "98"
127.0.0.1:6379> zpopmax zset     #弹出最大的元素
1) "hkt"
2) "200"   
127.0.0.1:6379> zrangebyscore zset 60 80 withscores  #列出某个分数范围的元素集合
1) "dsds"
2) "77"
zincrby zset 10 hkt   #给hkt加10分
zincrby zset -10 hkt  #给hkt减10分
127.0.0.1:6379> zrank zset hkt   #从小到大升序 查看排名情况
(integer) 3
127.0.0.1:6379> zrevrank zset hkt  #从大到小 降序查看排名情况
(integer) 0

应用场景

  • 排行榜
  • 带权重的消息队列

常用命令

nc localhost 6379  #如果没有cli脚本 可以直接使用nc命令与redis交互
keys *		#列出所有的key 生产一般不用
help @generic #查看帮助
flushall 	#删除所有数据
help @string  #查看字符串的相关本地方法帮助文档
help @list
help @hash
help @set
help @sorted_set
127.0.0.1:6379> type k1 #查看类型
string
127.0.0.1:6379> object encoding k1  #查看编码
"raw"