缓存最近热帖内容(hash)。
分布式锁(hash、string)。
记录帖子的点赞数,评论数和点击数(hash)。
记录用户帖子ID列表(zset)
记录帖子点赞用户id,评论id列表,用于显示和去重(zset)
记录帖子相关文章ID,根据内容推荐相关帖子(list)
帖子id自增,可以使用redis来分配帖子ID(计数器)
收藏集和帖子之间关系(zset)
记录热帖列表,总热榜和分类热榜(zset)
缓存用户历史行为,过滤恶意行为(zset、hash)
消息队列(zset、list、set),延迟队列(zset)
限流(hash)
5种基础数据结构:string(字符串),list(列表),hash(散列),set(集合),zset(有序集合)
string(字符串)
动态字符串,类似于java的ArrayList
用途:缓存用户信息。
扩容规则:当长度小于1MB时,每次扩容为加倍当前容量,当长度超过1MB时,只会扩容1MB。最大容量为512MB。
>set name codehole
>get name codehole
>del name
>mset name1 boy name2 girl name3 unknown #合并设置
>mget name1 name2 name3 #合并取
>expire name 5 #设置过期时间
>setex name 5 codehole
>set age 10
>incr age # 自增
>incrby age 5 #值超出signed long最大和最小会报错
list(列表)
类似于java的LinkedList,链表,插入删除快,索引定位慢。
用途:异步队列
元素较少的时候使用ziplist,数据量较多时转为quicklist。quicklist 是将ziplist用指针链接起来形成的链表结构。既满足快速插入删除性能,又没用太大空间冗余。(后续有详细介绍)
>rpush books python java golang # 同lpush rpush+rpop 栈 rpush+lpop 队列
>lpop books # 同 rpop
>llen books
>lindex books 1 # 获取index元素,遍历,性能差
>lrange books 0 -1 # 获取区间内所有元素 0 -1 表示所有元素
>ltrim books 1 -1 # 保留区间内元素 0 -1 表示所有元素
>ltrim books 1 0 # 区间为负 清空
hash(散列)
类似于java的HashMap,扩充时采取的是渐进式rehash策略,同时保留新旧hash结构,逐步的将旧数据迁移到新hash。
用途:存储用户信息,可只取部分信息。
>hset books java "Thinking in java"
>hgetall books
>hlen books
>hmset books java "effective java" python "learning python"
>hset user-plane age 10 # 单个子key也可以进行计数
>hincrby user-plane age 1
set(集合)
相当于java中的HashSet,内部无序,且唯一。
>sadd books python
>smembers books # 列出所有元素,无序
>sismember books java # 判断是value否有存在,等同于contains()
>scard books # 统计长度
>spop books # 弹出一个元素
zset(有序集合)
类似于java中SortedSet和HashMap结合,保证唯一,且根据score权重排序。通过跳跃列表实现(后续有详细介绍)。
用途:储存value用户id和score成绩,可根据成绩排序;存储粉丝列表,value粉丝id,score关注时间,可按关注时间排序。
>zadd books 9.0 "Thinking in java"
>zrange books 0 -1 # 按正序排序列出 参数为区间范围
>zrevrange books 0 -1 # 按倒序排序列出
>zcard books # 统计个数
>zscore books "Thinking in java" # 查询score
>zrank books "Thinking in java" # 查询排名
>zrangebyscore books 0 0.9 # 根据分值区间遍历
>zrangebyscore books -inf 0.9 wihtscores # 根据分值区间遍历,同时返回分值
>zrem books "Thinking in java" # 删除元素
通用规则:
1.create if not exists
2.drop if no elements
过期时间:
过期时间以整个对象为单位,而不是某个子key
已设置过期时间,调用set,过期时间失效
>set codehole yoyo
>expire codehole 600 # 设置过期时间
>ttl codehole # 查询过期时间
>set codehole yyy
redis分布式锁本质是占坑,其他进行要占坑时,放弃或稍后再试,通过setnx占坑,del释放,为了防止del未被释放,可以给锁加过期时间。(后续有详细介绍)
>setnx lock:codehole true
>expire lock:codehole 5 # 设置过期时间
>del lock:codehole
>set lock:codehole true ex 5 nx # 同时设置锁和过期时间,原子操作
超时问题:超时操作还未完成,不要做较长任务。好像没用说什么特别好的解决办法。
可重入性:对客户端set方法进行包装,使用线程的ThreadLocal存储当前持有锁计数。
异步消息队列:用list作为异步消息队列使用。
队列为空:通过sleep让线程睡一下;也可以通过阻塞读,lpop/rpop。链接闲置过久会自动断开,阻塞读会抛异常。
锁冲突处理:直接抛出异常,让用户重试;sleep再重试;请求转至延时队列,避开冲突。
延时队列:通过zset实现,消息序列化为zset的value,到期处理时间作为score。通过多线程轮询zset获取到期任务进行处理。
用途:记录用户365天签到记录
就是普通的string,byte数组,只是可以把值看成位数组,位数组会自动扩充0。零存整取,零存零取,整存零取。
>setbit s 1 1
>setbit s 15 1
>get s # 整取
>getbit s 15 # 零取
>set w hello # 整存
>bitcount w # 统计1的位数
>bitcount w 0 0 # 统计第一个字符1的位数
>bitcount w 0 1 # 统计前两个字符1的位数
>bitpos w 0 # 第一个0位
>bitpos w 1 1 1 # 从第二个字符算起 第一个1位
TODO bitfield
用途:提供不精确的去重统计,如页面每天用户访问数量。
需要占用12KB存储空间,不要用于统计单个用户数据,标准误差0.81%。
pfmerge可将多个pf计数器累加形成新pf值。
实现原理:没看懂 TODO
>pfadd codehole user1
>pfcount codehole
判断某个值是否存在时,如果不存在则一定不存在,存在时,可能不存在。
用途:推荐新闻,看过不推荐;爬虫系统URL去重;垃圾邮件过滤。
>bf add codehole user1
>bf exists codehole user2
>bf.madd codehole user3 user4
>bf.mexists codehole user4 user5
原理:对应到redis中一个大型位数组和几个无偏hash函数,无偏hash函数可以将hash值算的比较均匀,元素映射的位置比较随机。添加元素时,将元素通过多个hash函数对key进行hash,将多个对应位置的值设为1。判断jey是否存在时,同样通过hash函数进行hash,取出对应位置的值,如果有一个为0表示这个key不存在,如果都是1,表示可能存在这个key。
无法重新扩容,如需重新扩容必须额外记录存储器中的所有元素。
用途:限制用户规定时间内操作允许次数。
public class SimpleRateLimiter {
private Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
/**
* 思想:zadd(用户id+行为)作为key的多个值,使用(zremrangebysecore key 0 当前时间-时间窗口长度 )来除去时间窗口外的记录
* zcard统计key中存在的value的个数即是用户请求的次数
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) {
String key = String.format("hist:%s:%s", userId, actionKey); // 生成key
long nowTs = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
pipe.zadd(key, nowTs, "" + nowTs); // 添加操作
pipe.zremrangeByScore(key, 0, nowTs - period * 1000); // 删除限定时间之外的操作
Response<Long> count = pipe.zcard(key); // 统计当前用的的操作次数
pipe.expire(key, period + 1); // 给当前操作设置过期时间
pipe.exec();
pipe.close();
return count.get() <= maxCount; // 如果当前有效期内操作数大于规定最大操作数,返回false
}
public static void main(String[] args) {
Jedis jedis = new Jedis();
SimpleRateLimiter limiter = new SimpleRateLimiter(jedis);
for(int i=0;i<20;i++) {
System.out.println(limiter.isActionAllowed("laoqian", "reply", 60, 5));
}
}
}
public class FunnelRateLimiter {
static class Funnel {
//容量
int capacity;
//流出速率
float leakingRate;
//剩余容量
int leftQuota;
//计算起始时间
long leakingTs;
public Funnel(int capacity, float leakingRate) {
this.capacity = capacity;
this.leakingRate = leakingRate;
this.leftQuota = capacity;
this.leakingTs = System.currentTimeMillis();
}
void makeSpace() {
long nowTs = System.currentTimeMillis();
long deltaTs = nowTs - leakingTs;
int deltaQuota = (int) (deltaTs * leakingRate);
if (deltaQuota < 0) { // 空了
//间隔时间太长,整数数字过大溢出
this.leftQuota = capacity;
this.leakingTs = nowTs;
return;
}
if (deltaQuota < 1) { // 满了
//腾出空间太小,最小单位是1
return;
}
//剩余容量 = 当前容量 + 流出速率 * 间隔时间
this.leftQuota += deltaQuota;
this.leakingTs = nowTs;
if (this.leftQuota > this.capacity) { // 超出则溢出
this.leftQuota = this.capacity;
}
}
//判断是否能加入交易
boolean watering(int quota) {
makeSpace();
if (this.leftQuota >= quota) {
this.leftQuota -= quota;
return true;
}
return false;
}
}
private Map<String, Funnel> funnels = new HashMap<>();
public boolean isActionAllowed(String userId, String actionKey, int capacity, float leakingRate) {
String key = String.format("%s:%s", userId, actionKey);
Funnel funnel = funnels.get(key);
if (funnel == null) {
funnel = new Funnel(capacity, leakingRate);
funnels.put(key, funnel);
}
//需要1个quota
return funnel.watering(1);
}
}
redis 4.0 提供了限流模块redis-cell
>cl.throttle plane:reply 15 30 60 1 # key:plane:reply 容量:15 速率:30 operations / 60 seconds 可选参数:1 (默认1)
GeoHash算法,将经纬度映射到一维数组,越靠近,映射后的点距离越近。
用途:附近的人。
内部实际是zset(skiplist),通过score排序可的附近坐标元素。
>geoadd company 116.48 39.99 juejin
>geodist company juejin jindong km # 获取两个地方的距离
>geopos company juejin # 获取位置
>geohash company juejin # 获取节点hash值,可自行转为经纬度
>georadiusbymember company juejin 20 km count 3 asc # 查询指定元素附近的其他元素(asc正序,desc倒序)
>georadiusbymember company juejin 20 km count 3 withscoord withdist withhash asc # withdis 显示距离
>georadius compant 116.48 39.90 20 km withdist count 3 asc # 获取某经纬度附近的元素
keys : 用于列出所有满足特定正则的key。
缺点:没用offset、limit,一次性突出所有满足条件的key;是遍历算法,单线程redis,数据量过大会导致redis服务卡顿。
解决办法:scan
通过游标分布进行,不会阻塞线程;提供limit参数;提供模糊匹配;返回结果可能重复;按槽数采用高进位加法遍历key存在的大字典。
127.0.0.1:6379> keys name*
1) "name"
2) "name2"
3) "name3"
4) "name1"
127.0.0.1:6379> scan 0 match name* count 2
1) "4" # 没有遍历完,
2) (empty list or set) # 不代表没有匹配的元素
Redis是个单线程程序。
所有数据都在内存中,所有运算都是内存级别的运输,所以快。对于时间复杂度为O(n)的指令一定要小心谨慎,避免使Redis造成卡顿。
Node.js, Nginx也是单线程,也都是高性能。
Redis是单线程,那如何处理高并发客户端连接?
通过”多路复用IO“。
阻塞IO:需要进行读写操作时线程阻塞,如果要读n个字,没有读到数据,那么线程会一直阻塞,直到新的数据返回或者连接关闭,读方法才会返回,线程才能继续处理。
非阻塞IO:读写方法不阻塞,而是取决于缓存区内有多少数据,或者有多少空间能够写数据。
事件轮询(多路复用IO):
读数据没有读完,当数据到来时,线程如何得知?当写缓存区满了,剩下的数据何时才能继续写?
操作系统提供给用户事件轮询API,通过轮询去获取可处理事件,无事件操作时,最多等待timeout的时间,线程处于阻塞状态,一旦期间有事件过来,立刻返回。时间过后还没事件到来,立刻返回。拿到事件,处理完事件后继续轮询。
Redis为每个客户端套接字都关联一个指令队列。
Redis同样为每个客户端套接字关联一个响应队列。
Redis定时任务记录在”最小堆“中,每个循环周期,Redis将最小堆中到达时间点的任务进行处理,处理完成后记录下最快需要执行任务的事件,也就是timeout参数,然后休眠timeout时间。
Redis作者认为数据库性能瓶颈并非网络流量,而是数据库自身逻辑处理,因此Redis使用了浪费流量的文本协议,依然不影响Redis高访问性能。
REST:redis序列化协议,REST将传输数据结构分为5种最小单元类型,单元结束统一加上回车换行符\r\n。
单行字符串以”+“符号开头
多行字符串以”$"符号开头,后面跟字符串长度
整数值以“:"符号开头,后跟整数的字符串形式
错误信息以“-”符号开头
数组以“*”号开头,后跟数组的长度
>scan 0
1) "0"
2) 1) "info"
2) "books"
3) "author"
序列化
*2
$1
0
*3
$4
info
$5
books
$6
author
Redis特点之一,保障数据不会因为故障而丢失。
有两种机制,快照和AOF日志。
快照是一次全量备份,AOF日志是连续的增量备份。快照是内存数据的二进制序列化形式,存储紧凑。AOF日志是内存数据修改的指令记录文本,长时间运行AOF日志会庞大无比,数据库加载AOF日志重放时时间会非常漫长,定期需要对AOF进行瘦身。
快照:
利用操作系统的COW(Copy On Write)机制实现快照持久化。
Redis持久化时调用glibc的fork函数产生子进程,通过子进程处理持久化操作,父进程处理客户端请求。父子进程在最开始的时候和连体婴儿一样共享资源。
AOF日志:
收到客户端指令,执行指令然后将日志存盘。重放AOF非常耗时,通过bgrewriteaof指令对AOF日志瘦身。
当程序对AOF日志进行写操作时,实际上是将内容写入一个内存缓存中,然后异步将数据刷回磁盘。
通过fsync(int fd)函数强制将缓存刷到磁盘,fsync操作很慢,一般生产环境每隔1s左右执行一次fsync。
快照通过开启子线程方式进行,遍历整个内存,大块写磁盘加重系统负担。
AOF的fsync是一个耗时的IO操作,会降低性能,也会增加系统IO负担。
因此,持久化操作主要在从节点进行。
Redis4.0提供混合持久化
将rdb文件内容和AOF日志文件存在一起,此时的AOF文件是子持久化开始到持久化结束这段时间发生的增量AOF日志。
通过改变指令列表的读写顺序大幅节省IO时间,打包发送读数据,打包发送写数据,减少网络次数以此节省IO时间。
write操作只负责将数据写到本地缓存,如果缓存满了,此时需要等待空闲缓存,这个等待时间是写操作的真正耗时。
read操作只负责从本地缓存中取数据,如果缓存为空,此时需要等待新数据到来,这个等待时间是读操作真正的耗时。
redis事务提供multi、exec、discard指令。multi指事务开始,exec指事务结束,discard指multi到discard之间操作都丢弃。在exec之前,所有操作都缓存在服务器的事务队列中,当接受exec时才开始执行事务队列,执行完毕后一次性返回所有运行结果。即使当中有操作失败,后续指令也会继续执行,也就是redis不保证原子性。
通过watch乐观锁解决并发问题。
watch在事务执行前盯住一个或多个变量,当事务exec时,检查watch的变量是否被修改,如果修改了,exec则返回null。watch必须在multi之前执行。
>watch books
ok
>incr books # 修改watch的变量
(integer) 1
>multi
ok
>incr books
QUEUED
>exec
(nil) # 执行失败
通过pubsub提供消息多播。
必须先启动消费者,再执行生产者。
消费者可以通过getMeassage()轮询获得消息,获取不到则休眠,还可以通过listen阻塞监听消息进行处理。
缺点:生产者发布消息,如果没用一个消费者,消息会被直接丢弃;如果有消费者,部分消费者挂掉,当重新连接时,丢失的消息无法获得;Redis宕机,PubSub的消息不会持久化。
Redis5.0提供Stream数据结构,提供持久化消息队列。(后续有介绍)
如果Redis内部管理的集合数据结构很小,会使用紧凑存储形式压缩存储。
当hash数据较小时,使用ziplist存储,key和value作为两个相邻的entity存储。
当zset数据较小时,使用ziplist存储,value和score作为两个相邻的entity存储。
当set元素都是整数且元素个数较少时使用intset紧凑型整数数组结构存储。如果set存储的是字符串,会立刻升级为hashtable结构。
当集合元素增加或者某个value值过大时,小对象存储会被升级为标准结构。
内存回收机制:
删除key后并不是马上被回收,操作系统以页为单位来进行回收,只要页上有一个key在使用,就不能被回收,虽然已删除的key未被立即回收,Redis会重新使用那些尚未被回收的空闲内存。
执行flushdb,所有key都被干掉了,大部分之前的页面都完全干净了,就会立刻被操作系统回收。
CAP原理
C:Consistent,一致性
A:Availability,可用性
P:Partiton tolerance,分区容忍性
当网络分区发生时,一致性和可用性两难全。
Redis主从数据是异步同步,分布式Redis不满足一致性,满足可用性。保证最终一致性,从节点发生故障重启后,会努力追赶主节点,最终从节点和主节点状态保持一致。
主从同步、从从同步
增量同步:主节点将自己状态产生修改的指令记录到本地内存buffer中,异步将buffer的指令同步到从节点,从节点一边同步指令流一边向从节点反馈同步偏移量。主节点的buffer是一个定长环形数组,如果数组满了会覆盖前面内容。如果从节点还没有同步指令就被覆盖了,将触发快照同步。
快照同步:在主节点先进行一次bgsave,将当前内存数据全部快照到磁盘文件,再将快照文件同步给从节点。从节点接受完快照文件后,开始进行一次全量同步,加载前清空数据库,加载完成后继续进行增量同步。
增加从节点:当从节点加入集群时,先进行一次快照同步,完成后再进行一次增量同步。
无盘复制:主节点快照同步时需要将内存文件写到本地磁盘,消耗性能,Redis支持无盘复制,也就是主服务器直接通过套接字将快照内容发送到从节点,主节点一边遍历内存,一边将序列化的内容发送到从节点,从节点将收到的内容存储到磁盘中,然后再进行一次性加载。
wait指令可以使异步复制变成同步复制。
主从保证最终一致性,sentinel提供高可用,发生故障时可以自动进行主从切换。
监控主从节点健康,当主节点挂掉时,自动选择最有从节点升级为主节点。客户端连接集群时先连接sentinel,通过sentinel获取主节点地址,然后与主节点进行交互。当主节点发生故障时,客户端会重新向sentinel获取新的主节点地址。
sentinel无法保证消息完全不丢失。
主节点断了,连接池建立连接时会查询主节点地址是否与内存中地址一致,如果变更,则断开所有连接,重新使用新地址。
主动切换主节点,主节点所有修改下执行会抛出ReadonlyError。客户端将重新获取主节点地址,重新连接。
Codis是一个代理中间件。负责将特定的key转发到指定的Redis实例。
Codis默认将所有key划分成1024个槽(slot),首先对客户端传过来的key进行crc32运算计算hash值,再将hash后的整数对1024取模得余数,余数就是对应槽位。
走代理,网络开销要大一点。数据在多个redis中,无法支持事务操作,rename也比较危险。
去中心化,客户端需要缓存槽位相关信息。直接定位key所在节点。
通过Gossip协议来刚播自己得状态以及改变对整个集群的认知。
Redis5.0推出Stream,支持多播的可持久化消息队列。
每个Stream可以挂多个消费组,每个消费组有个游标last_delivered_id在Stream数组之上往前移动,表示当前消费已经消费到哪条消息。消费组通过xgroup create创建。每个消费组是独立的,也就是每条消息都会被每个消费组消费到。同一个消费组内可以挂接多个消费组,组内的消费组是竞争关系。
通过Info指令了解Redis运行状态,分为9大块:
Server:服务器运行的环境参数。
Clients:客户端相关信息。
Memory:服务器运行内存统计数据。
Persistence:持久化信息。
Stats:通用统计数据。
Replication:主从复制相关信息。
CPU:CPU使用情况。
Cluster:集群信息。
KeySpace:键值对统计数量信息。
Info可以一次性获取所有信息,也可以按快获取信息。
>info # 获取所有信息
>info memory # 获取内存相关信息
>info replication # 获取主从复制相关信息
Redlock:
加锁时,会向过半节点发送set(key,value,nv=True,ex=xxx)指令,只要过半节点set成功,就认为加锁成功。释放锁时,需要向所有节点发送del指令。
需要向多个节点进行读写,性能比单Redis下降一点。
Redis将每个设置了过期时间的key放到一个独立的字典中,会定时遍历这个字典来删除过期的key。同时,还是用惰性策略删除key,也就是访问key时key过期了就删除key。
定时扫描策略:
每秒进行10次过期扫描,采取贪心策略:
从过期字典中随机选出20个key;
删除这20个key中已过期的key;
如果过期的key的比例超过1/4,那就重复1)。
Redis会持续扫描过期字典(循环多次),知道过期字典的key变得稀疏,才会停止。这就会导致线上读写请求卡顿。导致卡顿还有一个愿意,就是内存管理器需要频繁回收内存也,也会产生一定的CPU消耗。
当客户请求到来时,如果正好服务器进入过期扫描状态,客户端请求等待25ms之后才会进行处理,而客户端将超时时间设置得比较短,那就会出现大量链接因超时而关闭。
如果有大批量key过期,需要给过期的key设置一个随即范围。
从节点不会进行过期扫描。主节点在key到期时会在AOF文件中增加一条del指令同步到从节点,从节点通过执行这条指令来删除过期的key。
当实际内存超过maxmemory时,Redis提供几种可选策略:
noevction:不会继续服务写请求(del请求可以继续服务),读请求可以继续服务。默认淘汰策略。
volatile-lru:尝试淘汰设置了过期时间的key,最少使用的key优先淘汰。
volatile-ttl:尝试淘汰设置了过期时间的key,最少剩余寿命ttl优先淘汰。
volatile-random:尝试淘汰设置了过期时间的key,随机。
allkeys-lru:尝试淘汰所有key中最少使用的key。
allkeys-random:尝试在所有key中随机淘汰。
Redis使用的近似LRU算法:
当Redis执行写操作时,发现内存超出maxmemory,就进行一次LRU淘汰算法。随机采样5(可配置)个key,谈话淘汰最旧的key,淘汰之后还超出maxmemory,继续随机采样淘汰,直到低于maxmemory。
Redis并不是只有一个主线程,还有几个异步线程用于处理一些耗时的操作。
del一个大key时,可能会卡顿,Redis4.0引入unlink指令,对删除操作进行懒处理,丢给后台线程异步回收内存,如果key很小,和del一样,会立刻回收。
flushdb和flushall也是非常慢的操作,Redis4.0提供了异步化,在这两个指令后面增加async参数即可。
异步队列:主线程将对象引用从”大树“中摘除后,会将这个key的内存回收操作包装成一个任务,塞进异步任务队列,后台线程会从这个异步队列中取任务。
AOF日志的sync函数也比较耗时,Redis将这个操作移步到异步线程来完成。有一个独立的处理AOF Sync操作的异步线程。
可重命名指令防止入侵
>rename-command keys abc
>rename-command flushall ”“
增加密码访问限制
requirepass 123455
masterauth 123456