redis应用
- 五种数据结构:string/list/set/hash/zset
- string是动态字符串,可以通过append追加。类似于java中的arraylist,采用的是预分配冗余空间来减少内存空间的频繁分配,当字符串小于1M时,扩容都是加倍现有空间,当大于1M时,扩容是增加1M空间,redis字符串最大是512M。
- list相当与java中的linkedlist(其实不是,ziplist和quicklist),删除和插入非常快,定位索引慢
- hash,相当于java中的hashmap,但是不同的是,值只能是字符串,而且rehash的方式不同,采用的渐进式的rehash。
- zset。底层采用跳跃列表支持随机的插入和删除
- 以下几点:
-
list/set/hash/zset如果容器不存在就创建一个进行操作
-
list/set/hash/zset如果容器中元素没有了,立即删除元素,释放内存
-
设置了过期时间,又调用set方法修改他,那么过期时间失效。
-
分布式锁。使用setnx抢占锁,使用del删除锁
-
但是如果在del之前setnx抢占成功之后程序处理失败了,会导致锁永远释放不掉,这是后就会考虑给锁加一个失效时间;但是在加锁之后立刻程序挂掉了,没有执行失效时间,那么这个锁也永远释放不了,所以在redis2.8之前,redis社区为了解决这个想到了很多办法,2.8之后,在set方法上可以同时添加一个过期时间。
-
同时获得锁并设置了过期时间,但是在过期时间失效之前,还没处理完,就会有下一个锁进来,就会处理异常,这种情况之后再考虑。
-
可重入锁,指线程在持有锁的情况下,支持同一个线程的多次加锁。
- 事务
- 把一批操作加入到队列中,执行exec方法时,全部执行,其实相当于跟其他操作隔离开,不被其他操作影响。
- redis不能支持回滚,只支持放弃discard。当执行一批操作时,其中有一个操作执行失败,不会影响其他操作,比如对一个字符串加1。所以redis只是顺序的把这些操作一次性执行完,不能保证全部执行成功或全部执行失败,所以,可以说只有隔离性没有原子性。
- 使用pipeling。redis由于要执行一批操作,所以尽量使用管道pipeline
- watch。当对一个元素进行查找修改时,不如把一个value值在此基础上累加一个值,需要先查找出来然后进行累加修改,这样就会出现更新丢失的问题,这样可以使用watch盯住一个key,当更新时发现修改了返回null,可以重试执行。
- 内存回收机制。redis并不总是可以把空闲内存立即归还给操作系统,操作系统回收内存是以页为单位的,每个页4k大小,如果这个页上只要有一个key,就不会立马回收,但是会重用这个位置。flushdb会全部干掉,因为所有页都没有key了,内存会立马归还给操作系统。
- scan
- 为了解决keys带来的问题,在redis2.8引入了scan命令,他不会阻塞线程,也可以进行模式匹配,可以输入offset和limit,然后返回的第一个参数是下一次开始的offset,知道返回为0代表遍历结束,scan 0 match key99* count 1000
- redis中所有的key都会存储到一个很大的字典中,类似于java中的hashmap,但是rehash采用渐进式hash等。
- scan遍历顺序是采用高位进位加法,是为了防止扩容或缩容时数据的重复或遗漏问题。
- scan除了除了可以遍历容器中的集合之外,还可以遍历set、hash中的key,比如 zscan 遍历 zset 集合元素,hscan 遍历 hash 字典的元素、sscan 遍历 set 集 合的元素。
- 大key。在redis中要避免使用大key,主要体现在数据的迁移,导致卡顿,删除时回收对象导致卡顿,扩容时,需要申请一个大的内存空间导致卡顿等。
- 惰性删除del。删除key使用del大部分情况下会非常快,但是如果删除一个非常大的key时,就会出现单线程卡顿现象。在redis4.0引入unlink,可以在后台异步回收。
- flushdb flushall。异步flush添加async,不会阻塞线程。
- hyperLogLog。去重,统计数量约数。如果没有此方法,会使用set放进去,然后进行统计,这样会占用无比大的存储空间。
- 布隆过滤器。一批数据是否包含某一个值。
- 限流。比如一个网站60s之内发帖不能超过30个,需要使用限流
- 简单限流。使用zset
- 漏斗限流。redis4.0提供了漏斗限流redis-cell模块
- 位图
- Geohash
- info指令
- stream。借鉴了kafka的设计
rdb与aof
- cow(copyonwrite)是java中的一个策略,修改的时候copy一份形成一个新的内容再修改。java中有copyonwritearraylist和copyonwritearrayset。
- rdb是指快照。redis在使用快照持久化时,会生成一个子线程来进行快照,和父线程共享里面的代码段和数据段,此时新的修改请求父线程会copy一份出去,子线程看到的数据一直是在快照开始的那一刻的数据,待子线程完成此次快照之后,主线程会进行合并。
- aof,append of file。aof日志存储的是操作记录序列,只存储修改的操作。当redis接收到一个请求时,会先进行校验,没有问题会先写道aof日志,然后再执行,这样如果宕机,就可以通过回放aof指令来进行恢复。
- aof fsync。aof写到磁盘上是先写到Linux自己的一个内存缓存中,然后内核自己会异步把数据刷回到磁盘,但是这时可能会出现突然宕机就会造成数据丢失,Linux提供了一个fsync可以立刻强制刷回到磁盘,但这个操作io比较大,每条数据执行一次,就影响效率,让Linux自己刷回,又太慢,可以折中比如1S执行fsync一次
- aof重写。redis长时间运行,aof日志会非常大,这样宕机恢复会十分耗时,所以,需要对aof进行瘦身。redis提供了bgrewriteaof指令对aof进行重写,主要原理就是开辟一个子线程对该aof进行重写合并到一个新的aof文件,然后把重写过程中的增量指令写到新文件,之后新的aof文件替换旧的。
- 快照遍历整个内存,比较耗资源,aof的fsync是一个耗io的操作,频繁会降低redis性能,造成延时等。可以考虑使用主从结构,从节点进行持久化,从节点是备份节点,没有来自客户端的请求压力,资源比较充沛。但是从节点连不上,或是从节点连不上的时候主节点又宕机了,此时就会出现丢数据的情况。
- redis4.0混合持久化。使用快照回放的时候会丢失大量数据,因为每个快照间隔会比较大,使用aof又会太慢,所以从该版本把两个持久化进行融合,将rdb和aof存放在一起,而aof存放的不再是全量的数据,而是自最近一次rdb之后的数据。回放的时候先回放rdb后回放aof即可。
主从同步
- 增量同步。redis同步的是指令流,redis在主节点会把需要同步的操作记录在buffer中,然后同步到从节点,从节点反馈同步到的偏移量,但是该buffer是有大小限制的,如果达到大小会从头覆盖之前的记录。如果因为网络状况不好,同步延迟了,但是此时未同步的数据被新数据覆盖了,此时就需要进行快照同步了。
- 快照同步。会把内存中的数据全部写到磁盘上,然后进行同步,是一个十分耗资源的操作。从节点执行快照同步时,把历史数据全部清空,把快照数据全部加载一遍,然后进行增量同步,但是如果此时buffer又满了,又有数据被覆盖了,就会进入一个死循环。为了避免这种死循环,需要配置一个适合大小的buffer。
- 无盘复制。快照同步时,是十分耗资源的,会有十分严重的io操作,如果此时fsync操作,fsync操作会被延迟,严重影响redis性能,影响服务效率。所以从redis2.8.18开始,支持无盘复制,即快照操作不写到磁盘,而是遍历内存直接socket发送到从节点,从节点还是一样,先加载到磁盘,然后一次性加载。
- wait指令。主从复制是异步的,可以使用wait指令变为同步,即执行wait指令会进行同步操作。
过期与淘汰机制
- redis会把设置过期时间的key放到一个独立的字典中,定时的去扫描,扫描的时候会采用一种贪心的算法,比如先随机取出20个,如果到期的key超过四分之一,然后再取出20个,以此循环,直到过期的key变得十分稀疏,每次循环的时间和不能超过25ms
- 除了定时遍历外,还会使用惰性删除,即查找该key时,会先检查过期时间。
- 由于定时删除机制,如果同一时间过期的key设置的过多,会让每次扫描都达到25ms的话,性能就会影响不少,所以,过期的key不要大量集中。
- 从库的过期策略。主库一个key过期时,在aof文件中添加一条aof指令,然后同步到从库。如果没有及时同步,会造成数据不一致。
- 淘汰机制,如果内存不够用时,会有如下的淘汰机制可供选择:
- noeviction 不会继续服务写请求 (DEL 请求可以继续服务),读请求可以继续进行。这样可以保证不会丢失数据,但是会让线上的业务不能持续进行。这是默认的淘汰策略。
- volatile-lru 尝试淘汰设置了过期时间的 key,最少使用的 key 优先被淘汰。没有设置过期时间的 key 不会被淘汰,这样可以保证需要持久化的数据不会突然丢失。
- volatile-ttl 跟上面一样,除了淘汰的策略不是 LRU,而是 key 的剩余寿命 ttl 的值,ttl越小越优先被淘汰。
- volatile-random 跟上面一样,不过淘汰的 key 是过期 key 集合中随机的 key。
- allkeys-lru 区别于 volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰。
- allkeys-random 跟上面一样,不过淘汰的策略是随机的 key。
- volatile-xxx 策略只会针对带过期时间的 key 进行淘汰,allkeys-xxx 策略会对所有的key 进行淘汰。如果你只是拿 Redis 做缓存,那应该使用 allkeys-xxx,客户端写缓存时 不必携带过期时间。如果你还想同时使用Redis 的持久化功能,那就使用 volatile-xxx 策略,这样可以保留没有设置过期时间的 key,它们是永久的 key 不会被 LRU 算法淘 汰。