为什么需要Redis
-
数据从单表,演进出了分库分秒
-
MySQL从单机演进出了集群
- 数据量增加,读写数据压力的不断增加
-
数据分冷热——热数据:经常访问的
-
将热数据存储到内存中
Redis基本工作原理
-
数据从内存中读写
-
数据保存到硬盘上防止重启数据丢失
-
增量数据保存到AOF文件
- AOF (Append Only File): AOF persistence logs every write operation received by the server. These operations can then be replayed again at server startup, reconstructing the original dataset. Commands are logged using the same format as the Redis protocol itself.
-
全量数据RDB文件
- RDB (Redis Database): RDB persistence performs point-in-time snapshots of your dataset at specified intervals.
-
-
单线程处理所有操作命令——排队按顺序执行
Redis 中 String 数据结构
-
数据结构sds
-
可以存储字符串、数字、二进制数据
-
通常和expire配合使用
-
场景:存储技术、Session
-
寻址:指针指向buf前 → flag中 低三位存sdshdr的类型
-
alloc——后面的buf实际长度
-
len——后面的buf实际使用的空间
Redis 中 List 数据结构——Quicklist
由一个双向链表和listpack实现
Redis 中 Hash 数据结构——dict
- rehash:rehash操作是将ht[0]中的数据,全部迁移到ht[1]中。数据量小的场景下,直接将数据从ht[0]拷贝到ht[1]速度是较快的。数据量大的场景,例如存有上百万的KV时,迁移过程将会明显阻塞用户请求
- 渐进式rehash:为避免出现这种情况,使用的rehash方案。其原理就是,每次用户访问时都会迁移少量数据。将整个迁移过程,平摊到所有的访问用不请求过程中
- rehash的原因:hashtable需要扩容
Redis 中 zset 数据结构——zskiplist
实际案例
连续签到
- 实现获取用户连续签到天数
- 到时过期
// increase the continuous check-in days by 1
RedisClient.Incr(ctx, key)
// set the expire time
RedisClient.ExpireAt(ctx, key, expAt)
消息通知
用list作为消息队列
-
使用场景:消息通知。例如当文章更新时,将更新后的文章推送到ES,用户就能搜索到最新的文章数据
计数
一个用户有多想计数需求,可以通过hash结构存储
-
Redis is a TCP server using the client-server model and what is called Request/Response protocol.
To accomplish a request
- The client sends a query to the server, and reads from the socket, usually in a blocking way, for the sever response
- The server processes the command and sends the response back to the client.
-
使用pipeline打包一次性发送多条请求到服务器
func Ex06InitUserCounter(ctx context.Context) { pipe := RedisClient.Pipeline() userCounters := []map[string]interface{}{ {"user_id": "1556564194374926", "got_digg_count": 10693, "got_view_count": 2238438, "followee_count": 176, "follower_count": 9895, "follow_collect_set_count": 0, "subscribe_tag_count": 95}, {"user_id": "1111", "got_digg_count": 19, "got_view_count": 4}, {"user_id": "2222", "got_digg_count": 1238, "follower_count": 379}, } for _, counter := range userCounters { uid, err := strconv.ParseInt(counter["user_id"].(string), 10, 64) key := GetUserCounterKey(uid) rw, err := pipe.Del(ctx, key).Result() if err != nil { fmt.Printf("del %s, rw=%d\n", key, rw) } _, err = pipe.HMSet(ctx, key, counter).Result() if err != nil { panic(err) } fmt.Printf("设置 uid=%d, key=%s\n", uid, key) } // 批量执行上面for循环设置好的hmset命令 _, err := pipe.Exec(ctx) if err != nil { // 报错后进行一次额外尝试 _, err = pipe.Exec(ctx) if err != nil { panic(err) } } }
排行榜
积分变化时,排名要实时变更
- 结合dict后,可实现通过key操作跳表的功能
限流
-
要求1秒内放行的请求为N,超过N则禁止访问
-
key: comment_freq_limit_1671356046(时间戳)
对这个key调用incr,超过限制N则禁止访问
分布式锁
并发场景,要求一次只能有一个协程执行。执行完成后,其他等待中的写成才能执行
可以使用redis的setnx实现,利用了两个特性
-
Redis时单线程执行命令
-
setnx只有未设置过才能执行成功
Redis使用注意事项
大Key
| 数据类型 | 大Key标准 |
|---|---|
| String类型 | value的字节数大于10KB |
| Hash/Set/Zset/list等复杂数据结构类型 | 元素个数大于5000个或总value字节数大于10MB |
-
大Key的危害
- 读取成本高
- 容易导致满查询(过期、删除)
- 主从复制异常,服务阻塞,无法正常响应请求
-
业务侧使用大Key的表现
- 请求Redis超市报错
-
消除大Key的方法
-
拆分
- 将大可以拆分为小key。例如一个string拆分成多个
-
压缩
- 将value压缩后写入Redis,读取时解压后再使用。压缩算法可以时gzip,snappy、lz4等。
- 通常情况下,一个压缩算法压缩率越高、则解压好事就越长。需要对实际数据进行测试后,选择一个合适的算法
- 如果存储的是JSON字符串,可以考虑吧使用MessagePack进行序列化
-
集合类结构hash、list、set、zset
- 拆分:可以用hash取余,位掩码的方式决定放在哪个key中
- 区分冷热;如榜单列表场景使用zset,只缓存前10页数据,后续数据走database
-
热Key
-
热Key:用户访问一个Key的QPS很高(超500),导致Server实例出现CPU负载突增或者不均的情况。
-
解决热Key的方法
-
设置Localcache
在访问Redis前,在业务服务侧设置Localcache,降低访问Redis的QPS。Localcache中缓存过期或未命中,则从Redis中将数据更新到Localcache。Java的Guava,Golang的Bigcache就是这类的Localcache
-
拆分
将key:value这一个热key复制写入多份,如key1:value, key2:value,访问的时候访问多个key,但value是同一个,一次奖QPS分散到不同实例上,降低负载。代价是,更新时需要更新多个key,存在数据短暂不一致的风险
-
使用Redis代理的热Key承载能力
结合“热Key发现”、“Localcache”两个功能
-
慢查询场景
-
容易导致redis慢查询的操作
-
批量操作一次性传入过多的key/value,如mset/hmset/sadd/zadd等O(n)操作
建议单批次不要超过100,超过100后性能明显下降
-
zset大部分命令都是O(log(n)),当大小超过5k以上时,简单的zadd/zrem也可能导致慢查询
-
操作的单个value过大,超过10kb,避免使用大Key
-
对大Key的delete/expire操作
-
缓存穿透、缓存雪崩
-
缓存穿透:热点数据查询绕过缓存,直接查询数据库
-
查询一个一定不存在的数据
- 通常不会缓存不存在的数据,这类查询请求都会直接打到db,如果有系统bug或人为攻击,那么容易导致db响应慢甚至宕机
-
缓存过期时
- 在高并发场景下,一个热Key如果过期,会有大量请求同时击穿至db,容易影响db性能和稳定
- 同一时间有大量key集中过期时,也会导致大量请求落到db上,导致查询变慢,甚至出现db无法响应新的查询
- 缓存空值:如一个不存在的userID。这个id在缓存和数据库中都不存在,下次再查缓存直接返回空值
- 布隆过滤器:通过bloom filter算法来存储合法Key,得益于该算法超高的压缩率,只需占用极小的空间就能存储大量key值
-
-
缓存雪崩:大量缓存同时过期
-
缓存空值
- 将缓存失效时间分散开,比如在原有的失效时间基础上增加一个随机值。对于热点数据,过期时间尽量设置的长一些
-
使用缓存集群,避免单机宕机造成的缓存雪崩
-