02 Redis.txt

UP 返回
视频4-1
1 主流应用架构
	客户端	缓存器	存储层
  客户端向缓存器请求数据,如果存在则直接返回;不存在则穿透查询存储层,查询的结果再回写到缓存层(回种),并返回客户端;同时还有熔断机制(当存储层不能提供服务时,客户端的请求直接由缓存层处理,不论有没有结果都直接返回,从而能在有损的情况下对外提供服务)

2 缓存中间件
  Memcache	代码层次类似于hash
	支持简单数据类型(string);不支持数据持久化存储;不支持主从同步;不支持分片(打碎数据库,将大数据分布到多个物理节点上的方案)
  Redis
	数据类型丰富(set list等);支持数据磁盘持久化存储;支持主从同步;支持分片(redis3.0开始)

  有数据持久化的需求或者对数据结构处理有高级要求的使用redis,其他简单的key-value存储直接用memcache即可

3 redis为什么快
  redis拥有100000+QPS (query per second,每秒内查询次数)
	完全基于内存,绝大部分请求是纯粹的内存操作,执行效率高,不会受到硬盘IO的影响(单继承单线程的c语言编写)
	数据结构简单,对数据操作也简单(存储结构就是键值对,类似于HashMap,查找操作复杂度基本是O(1)。性能比关系型数据库要高很多)
	采用单线程,单线程也能处理高并发请求,想多核也可启动多实例
		主线程是单线程(包括IO事件的处理以及IO对应的相关业务的处理,过期键的处理,父子协调,集群协调等,这些逻辑会被封装成周期性的任务由主线程周期性的处理)。这样对客户端所有读写请求就是串行的处理,多个客户端的写操作就不会有并发问题,避免了频繁的上下文切换和锁竞争,效率更高。
		单线程是指处理网络请求的时候单,但是实际运行的时候肯定不止一个线程。比如进行数据持久化时,会根据实际情况以子进程或子线程的方式执行
	使用多路I/O复用模型,非阻塞IO
		redis需要在多个平台运行,所以会根据编译平台的不同选取不同的IO多路复用函数作为子模块,提供给上层统一调用的接口
		会优先使用时间复杂度为O(1)的I/O多路复用函数作为底层实现(时钟的evport,linux中的epoll,macOS中的kqueue,这些性能都比select优秀)
		因为select在所有系统上都会实现,所以以时间复杂度为O(n)的select作为保底方案。一旦当前系统没有上述比select更优秀的函数就选择select作为备选方案
		基于react设计模式监听I/O事件			

视频4-2
				dbsize				查看redis数据量
4 redis数据类型			底层的数据类型有:简单动态字符串;链表;字典;跳跃表;整数集合;压缩列表;对象
	String	最基本的数据类型,即键值对,值最大能存储512m;二进制安全,所以可以包含任何数据,比如jpg图片或者序列化的对象
				set name "redis"		设置一个值为redis的name键
				get name				输出redis
				set count 1				设置count值为1
				get count
				incr count				count自增(redis的单个操作都是原子性的,即一个事务是一个不可分割的最小工作单位,事务中包括的操作要么都做要么都不做,所以不用考虑并发问题)
				get count
				比如需要统计某个页面用户每天的访问次数,只需要incr userId201231(userId+当日的时间戳),这样就可以统计了
		保存字符串对象的结构:
			struct sdshdr{
				int len;//buf中已占用空间的长度
				
				int free;//buf中剩余可用空间的长度
				
				char buf[];//数据存储空间
			};
	Hash		String元素组成的字典,适用于存储对象
				hmset lilei name "Lilei" age 26 title "Senior"		设置一个叫lilei的hash,拥有属性name age title
				hget lilei name										打印name属性
				hset lilei title "Pricipal"								修改title属性
				hget lilei title
	List		列表,按照String元素插入顺序排序。大约能存储40亿个成员
				lpush mylist aaa					向列表mylist添加元素aaa
				lpush mylist bbb				
				lpush mylist ccc
				lrange mylist 0 10					从mylist第0位取出10个元素(这个mylist只会打印三个元素,同时元素是后进先出,即ccc bbb aaa)
				可以实现最新消息排行板的功能,因为越新插入的消息越先显示
	Set		string元素组成的无序集合,通过哈希表实现,不允许重复。删除查找O(1)
				sadd myset 111		向myset插入111
				sadd myset 222
				sadd myset 333
				sadd myset 222		此次插入失败,元素重复
				sadd myset abc
				sadd myset abd
				smembers myset		查看set所有元素。是无序的
				在微博应用中可以将一个用户的所有关注人存在一个集合中,所有粉丝存在一个集合中。redis为集合提供了方便的交并差等操作,就能很好实现共同关注共同喜好等功能
	Sorted Set	通过分数来为集合中的成员进行从小到大的排序。不允许重复元素,分数可以重复
				zadd myzset 3 abc					插入元素abc,分数为3
				zadd myzset 1 abd
				zadd myzset 2 abb
				zadd myzset 1 abd				此处插入失败
				zadd myzset 1 bgg	
				zrangebyscore myzset 0 10		查看元素(会以分数从低到高排序)
				可以用来存储全班同学的成绩,score为得分,value为学号,这样添加完毕以后就已经排好序了。还可以用这个来做带权重的队列,比如给不同任务分配不同的分数来优先执行
	用于计数的HyperLogLog,用来支持存储地理位置信息的Geo

视频4-3
5 从海量key里查询某一固定前缀的key
  首先要摸清数据规模,即问清楚边界
  如果使用keys pattern指令会一次性返回所有匹配的key ,键的数量过大会使服务卡顿,对内存的消耗和redis服务器都是隐患。
  所以使用SCAN cursor [MATCH pattern][COUNT count],可以无阻塞的提取出符合条件的key列表,每次执行只会返回少量key,不会像keys可能阻塞服务器
	基于游标的迭代器,需要基于上一次的游标延续之前的迭代过程
	以0作为游标开始一次新的迭代,直到命令返回游标0完成一次遍历
	不保证每次执行都返回某个给定数量的元素,支持模糊查询。甚至可能返回0个元素,但是只要返回的游标不为0,当前遍历就不应该结束。数量多的时候可能一次返回十几个,数量少的时候可能一次全部返回,等同于keys
	一次返回的数量不可控,只能是大概率符合count参数
		scan 0 match a* count 4			返回符合a*的key集合。该语句会返回两个值,第一个表示本次遍历以后的游标,用于下次遍历,后一个返回值为返回的key
			例如返回:1)	“2”
						2)1)“a9”
			那么下一次遍历应该是scan 2 match a* count 4。同时每一次返回的游标不一定会比上一次的小,所以可能会返回重复的key,需要在外部程序自己去重

视频4-4
6 通过redis实现分布式锁
  分布式锁需要解决的问题:互斥性(任意时刻只能由一个客户端获取锁);安全性(锁只能被持有该锁的客户端删除);死锁(获取锁的程序因为某些原因宕机无法释放锁,其他客户端可能再也无法获取锁,需要处理这种情况);容错(当部分redis结点宕机时,客户端仍然能获取锁和释放锁)
	SETNX key value		如果key不存在,则创建并赋值。复杂度O(1),设置成功返回1,失败返回0
			 setnx locknx task		设置key,返回1
			 setnx locknx test		设置失败,返回0
		这样在执行某一个任务前,可以先使用setnx命令,判断当前是否有锁。但是这样设置的key是长期有效的,所以需要设置过期时间
	EXPIRE key seconds	设置key的过期时间的秒数,当key过期时会被自动删除
			expire locknx 2
			setnx locknx test		2s以后设置成功,返回1。业务中可能出现的伪代码:
				RedisService redisService = SpringUtils.getBean(RedisService.class);
				long status = redisService.setnx(key,"1");
				if(status == 1){
					redisService.expire(key,expire);
					doOcuppiedWork();//进行独占资源的逻辑
				}
				以上代码如果在status获取成功以后出错了,key仍然将一直被占用。出现的原因就是原子性得不到满足,虽然setnx和expire本身是原子的,但是在这里被组合以后就不是了
	SET key value [EX seconds] [PX milliseconds] [NX|XX]		EX表示过期时间的秒,PX表示过期时间的毫秒,NX表示只在键不存在时才对键进行设置操作,XX只在键存在时才进行操作。操作成功返回OK,失败返回nil。从redis2.6.12版本开始允许将两个操作变为一个操作执行
			set locktarget 12345 ex 5 nx			设置键locktarget不存在时则将其设为12345(一般可以将其值设置成对应的requestid或线程id,即可以标识当前占用该资源的请求或线程),过期时间为5s。返回OK
			set locktarget 12345 ex 5 nx			继续设置5s内会失败返回nil,5s后设置成功。业务中的伪代码:
				RedisService redisSerivce = SpringUtils.getBean(RedisService.class);
				String result=redisService.set(lockKey,requestId,SET_IF_NOT_EXIST,SET_WITH_EXPIRE_TIME,expireTime);
				if("OK".equals(result)){
					doOcuppiedWork();//进行独占资源的逻辑
				}
				以上代码就可以变成原子操作了
  如果大量的key同时过期,由于清除大量的key很耗时,会出现短暂的卡顿现象。可以在设置过期时间的时候给每个key加上随机值,可以很大程度避免

视频4-5
7 通过redis做异步队列
  使用List作为队列,RPUSH生产消息,LPOP消费消息
			rpush testlist aaa
			rpush testlist bbb
			rpush testlist ccc
			lpop testlist					重复执行可以像队列一样取出数据。但是缺点是没有等待队列里有值就直接消费,可以通过在引用层引入sleep机制去调用lpop重试
	BLPOP	key [key ...] timeout		阻塞直到队列有消息或者timeout后超时(单位s)
			blpop testlist 30			此时若testlist没有元素则会超时,30s后会结束;如果中途有元素了则直接取出。但是缺点是只能供一个消费者消费
  如果想让多个消费者来消费,可以使用pub/sub主题订阅者模式(发送者pub发送消息,订阅者sub接收消息;订阅者可以订阅任意数量的频道)
			subscribe myTopic				在客户端1订阅主题myTopic(该主题无需事先存在)
			subscribe myTopic				在客户端2订阅主题myTopic
			subscribe anotherTopic		在客户端3订阅主题anotherTopic
			publish myTopic "Hello"		在客户端4向myTopic频道发布消息,客户端1 2将收到
			publish myTopic "I love you"	
			publish anotherTopic "hi"		在客户端4向anotherTopic频道发布消息,客户端3将收到
	缺点:消息的发布是无状态的,无法保证可达(对发布者来说消息是即发即失的;生产者在消息发布时下线,重新上线是无法接收到的)。要解决这个问题需要使用专业的消息队列,如卡夫卡等

视频4-6
8 redis持久化
  RDB(快照)持久化:保存某个时间点的全量数据快照
	打开reids的配置文件,其中save的策略信息:
	save 900 1				表示900s之内如果有1条写入指令,则进行一次快照,即备份。下面同理(redis每个时段的读写请求是不同的,所以可以根据需要配置多条规则)
	save 300 10
	save 60 10000
	stop-writes-on-bgsave-error yes	设置yes表示当备份进程出错时,主进程停止接收新的写入操作(为了保护持久化数据一致性的问题。如果自己的业务有完善的机制处理这种问题可以关闭这个设置)
	rdbcompression yes				表示备份时需要将rdb文件压缩后再做保存(建议设置为no,因为redis本身是cpu密集型服务器,开启压缩会占用更多的cpu效率,相比硬盘成本cpu的速度更重要)
	save "" 							禁用上述的rdb配置只要加上 save "" 即可
	src目录下有一个二进制文件dump.rdb,redis会定期将数据备份到这里。rdb文件可以通过以下命令生成:
		SVAE		该命令会阻塞redis的服务器进程,直到rdb文件被创建完毕,很少被使用
		BGSAVE	fork出一个子进程来创建rdb文件,不阻塞服务器进程(父进程处理自身请求的同时,会通过轮询来接收子进程的信号。子进程文件生成以后会立刻返回OK给父进程)
					bgsave命令执行时会先检查有没有aof/pdb的子进程在执行,如果已经有了备份任务,将会拒绝客户端的save bgsave命令(防止子进程竞争);如果没有就调用源码的rdbSaveBackground来调用系统的fork指令创建进程,实现了copy-on-write
		lastsave	可以通过此命令查询上次备份时间(返回的是一个integer,如果值不同说明有了新的备份。可以通过这个时间戳生成不同的时间的全量备份文件)
	自动触发rdb持久化的方法:
		根据redis.conf配置里的SAVE m n定时触发(使用的是BGSAVE命令)
		主从复制时,主节点自动触发
		执行Debug Reload
		执行Shutdown且没有开启AOF持久化
	缺点:内存数据的全量同步,数据量大时会由于I/O而严重影响性能;可能会因为redis挂掉而丢失从当前至最近一次快照期间的数据

视频4-7	
  AOF(Append-Only-File)持久化:保存写状态
	记录下除了查询以外的所有变更数据库状态的指令
	以append的形式追加保存到AOF文件中(增量)
	aof持久化默认是关闭的,可以修改redis配置文件中的appendonly no为yes即可开启。文件名的配置为appendfilename "appendonly.aof"。文件写入方式的配置项如下:
	# appendfsync always		一旦缓存区的内容发生变化,就及时的将缓存区的内容写入aof
	appendfsync everysec		每隔1s将缓存区的内容写入文件(默认且推荐的方式)
	# appendfsync no			将写入aof的操作交给操作系统决定。一般为了效率,操作系统会等到缓存区满了才会将数据一次性写入文件
	可以通过config命令修改配置参数:config set appendonly yes
	AOF会实时将命令记录下来,文件就会不断增大,redis解决了这个问题,详见视频

  redis数据的恢复
	redis启动会优先加载aof文件,没有才会加载rdb文件

  RDB和AOF对比:
	RDB全量数据快照,文件小,恢复快;但是无法保存最近一次快照之后的数据
	AOF可读性高,适合保存增量数据,数据不易丢失;但是文件体积大,恢复时间长

  RDB-AOF混合持久化方法:redis4.0后推出,并作为默认的方式
	BGSAVE做镜像全量持久化,AOF做增量持久化。redis实例重启时,会先bgsave持久化文件重新构建内容,再使用aof文件重放近期的操作指令,就能完整恢复重启之前的状态

视频4-8
9 Pineline
	Pineline和linux的管道类似
	redis基于请求/响应模型,单个请求处理需要一一应答
	pineline批量执行指令,节省多次IO往返时间
	有顺序依赖的指令建议分批发送

10 redis的同步机制

	 









DOWN 返回