Redis大家都很熟悉最常用的就是做缓存但是Redis的功能远不止缓存它有丰富的数据结构高性能持久化发布订阅等等能用在很多场景比如分布式锁消息队列排行榜计数器限流社交关系最近列表等等用好Redis能大大提升系统的性能和开发效率。
我用Redis很多年了除了缓存也用在很多其他场景踩了一些坑也总结了一些经验今天就来分享一下Redis的高级用法不只是缓存这些场景你用过吗?
一、Redis的数据结构
在讲高级用法之前先简单回顾一下Redis的基本数据结构因为高级用法都是基于这些数据结构的。
Redis有五种基本数据结构:
- String(字符串): 最基本的类型能存字符串整数浮点数二进制数据最大能存512MB常用的命令有setgetincrdecrappendstrlen等等。
- Hash(哈希): 键值对的集合适合存对象比如用户信息商品信息等等常用的命令有hsethgethgetallhdelhlenhexistshincrby等等。
- List(列表): 有序的字符串列表按插入顺序排序可以从头部或尾部添加删除适合做消息队列最近列表等等常用的命令有lpushrpushlpoprpoplrangellenlindex等等。
- Set(集合): 无序的字符串集合元素不重复适合做去重交集并集差集社交关系等等常用的命令有saddsremsmemberssismemberscardsintersunionsdiff等等。
- Sorted Set(有序集合): 有序的字符串集合每个元素都有一个分数(score)按分数排序元素不重复分数可以重复适合做排行榜范围查询等等常用的命令有zaddzremzrangezrevrangezrangebyscorezscorezrankzincrby等等。
除了这五种基本数据结构Redis还有一些高级数据结构比如Bitmap(位图)HyperLogLogGeo(地理位置)Stream(流)等等这些也很有用。
二、分布式锁
分布式锁是分布式系统中很常用的一个功能用来保证在分布式环境下同一时间只有一个客户端能执行某个操作避免并发问题。
Redis因为高性能原子操作所以很适合做分布式锁。
实现方式:
最简单的分布式锁就是用setnx命令setnx是set if not exists的意思也就是只有key不存在的时候才设置成功如果key已经存在就设置失败这样同一时间只有一个客户端能设置成功也就是获取到锁。
但是简单的setnx有一个问题就是如果获取锁的客户端挂了锁就一直不释放其他客户端就永远获取不到锁了所以要给锁加一个过期时间到时间自动释放。
在Redis 2.6.12之前setnx和expire是两个命令不是原子的如果setnx成功了但是expire失败了锁就没有过期时间了还是会有问题所以Redis 2.6.12之后set命令支持nx和ex参数能一次性设置key不存在才设置和过期时间是原子的这样就安全了。
所以正确的获取锁的方式是:
SET lock_key unique_value NX EX 30这里lockkey是锁的keyuniquevalue是一个唯一的值用来标识是哪个客户端加的锁释放锁的时候要判断是不是自己加的锁NX是key不存在才设置EX 30是过期时间30秒。
释放锁的时候不能直接del因为可能锁已经过期了被其他客户端获取了这时候直接del就会把别人的锁删掉了所以要先判断锁的value是不是自己的如果是再删除这个判断和删除要原子的所以要用Lua脚本。
释放锁的Lua脚本:
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end这样就安全了。
注意事项:
- 过期时间要合理: 过期时间不能太短太短业务还没执行完锁就释放了会有并发问题也不能太长太长客户端挂了锁要等很久才释放影响可用性要根据业务的执行时间合理设置一般比业务最长执行时间长一点。
- value要唯一: 每个客户端获取锁的时候value要唯一比如UUID这样释放锁的时候才能判断是不是自己的锁避免误删别人的锁。
- 要考虑锁续期: 如果业务执行时间不确定可能超过过期时间要考虑锁续期也就是在业务执行过程中定期给锁延长过期时间比如Redisson的看门狗机制就是自动续期的。
- Redis主从切换的问题: 如果Redis是主从架构主节点加锁成功了但是还没同步到从节点主节点就挂了从节点变成主节点这时候锁就丢了其他客户端又能获取锁了会有并发问题这个问题Redis的RedLock算法试图解决但是也有争议如果对一致性要求特别高可能要用ZooKeeper等其他方案。
三、消息队列
Redis也能做消息队列虽然不如专业的消息队列比如RabbitMQKafka功能强大但是对于一些简单的场景Redis的消息队列足够用了而且轻量方便性能也不错。
Redis做消息队列有几种方式:
1. List实现的简单消息队列:
用List的lpush和rpop或者rpush和lpop就能实现一个简单的消息队列生产者用lpush往列表左边加消息消费者用rpop从列表右边取消息这样就是一个先进先出的队列。
但是简单的rpop有一个问题就是如果队列里没有消息rpop会立即返回null消费者就要不断轮询很浪费CPU所以Redis提供了brpop命令也就是阻塞式的rpop如果队列里没有消息就阻塞等待直到有消息或者超时这样就不用轮询了节省CPU。
所以用List + brpop就能实现一个简单的阻塞式消息队列适合大部分简单场景。
2. Pub/Sub(发布订阅):
Redis还有Pub/Sub功能也就是发布订阅生产者往某个channel发布消息所有订阅了这个channel的消费者都能收到消息这是一个广播模式的消息队列适合一对多的场景。
Pub/Sub的命令有publish(发布)subscribe(订阅)unsubscribe(取消订阅)psubscribe(模式订阅)等等。
但是Pub/Sub有一个缺点就是消息是即时的不会持久化如果消费者不在线消息就丢了而且消费者处理不过来消息也会丢所以Pub/Sub只适合一些不要求消息可靠的场景比如实时通知等等。
3. Stream(流):
Redis 5.0引入了Stream数据结构这是一个专门为消息队列设计的数据结构功能很强大支持持久化确认机制消费者组等等基本上能替代专业的消息队列大部分功能。
Stream的命令有xadd(添加消息)xread(读取消息)xgroup(创建消费者组)xreadgroup(消费者组读取)xack(确认消息)等等。
Stream支持消费者组多个消费者组成一个组共同消费一个队列里的消息每条消息只会被组里的一个消费者消费而且支持消息确认消费者处理完消息要xack确认没确认的消息会保留能重新投递这样就保证了消息不会丢很可靠。
如果你用的是Redis 5.0以上需要一个可靠的消息队列Stream是一个很好的选择。
注意事项:
- 消息可靠性: 用List做的简单队列消息可能会丢比如消费者取到消息还没处理就挂了消息就丢了如果对消息可靠性要求高要用Stream或者专业的消息队列。
- 消息积压: Redis的消息队列不适合消息大量积压的场景因为Redis是内存数据库消息都存在内存里积压太多会占用大量内存甚至把内存用光如果有大量消息积压的场景要用专业的消息队列比如Kafka。
- 消费者幂等: 不管用什么消息队列消费者都要保证幂等也就是同一条消息处理多次结果一样因为网络问题重试等等消息可能会重复投递幂等能避免重复处理带来的问题。
四、排行榜
排行榜是很多系统都有的功能比如游戏的积分榜电商的销量榜社区的热门帖等等Redis的Sorted Set(有序集合)天生就适合做排行榜。
Sorted Set每个元素都有一个分数按分数排序能快速获取某个元素的排名分数能快速获取前N名能快速获取某个分数范围的元素等等非常适合做排行榜。
实现方式:
比如做一个游戏的积分榜每个玩家有一个分数我们用一个Sorted Setkey是game:rankmember是玩家IDscore是分数。
玩家得分了就用zincrby给玩家加分:
ZINCRBY game:rank 10 player1这样player1的分数就加了10。
获取前10名从高到低:
ZREVRANGE game:rank 0 9 WITHSCORES这样就能获取前10名和他们的分数。
获取某个玩家的排名:
ZREVRANK game:rank player1这样就能获取player1的排名从0开始。
获取某个玩家的分数:
ZSCORE game:rank player1这样就能获取player1的分数。
获取分数在某个范围的玩家:
ZRANGEBYSCORE game:rank 100 200 WITHSCORES这样就能获取分数在100到200之间的玩家。
是不是很简单很方便?
进阶用法:
- 每日/每周/每月排行榜: 如果需要每日每周每月的排行榜可以用不同的key比如game:rank:20170306(今日)game:rank:201703w10(本周)game:rank:201703(本月)这样就能分别统计不同时间维度的排行榜过期的排行榜可以自动删除或者归档。
- 排行榜分页: 如果排行榜人很多需要分页可以用zrevrange的start和stop参数比如第1页0-9第2页10-19等等很方便。
- 相同分数的排序: Sorted Set分数相同的元素是按member的字典序排序的如果需要相同分数按其他方式排序比如按达到分数的时间早的排前面可以把时间也编码到分数里或者用两个Sorted Set组合。
注意事项:
- 内存占用: Sorted Set是基于跳表和哈希表实现的内存占用比普通的集合大一些如果排行榜的元素特别多比如几千万要注意内存占用可以考虑只保留前N名或者用其他方案。
- 持久化: Redis的数据是在内存里的虽然有持久化但是还是可能会丢数据如果排行榜的数据很重要不能丢要做好持久化或者同时存一份到数据库里。
- 并发更新: zincrby是原子的所以并发更新没问题不用担心并发问题。
五、计数器
计数器也是很常用的功能比如文章的阅读量点赞数评论数网站的访问量用户的在线时长等等Redis的String类型incr/decr命令天生就适合做计数器因为incr/decr是原子的性能很高。
实现方式:
比如文章的阅读量每篇文章一个keyarticle:view:123value是阅读量。
用户访问文章就incr一下:
INCR article:view:123这样阅读量就加1了incr是原子的并发也没问题。
获取阅读量:
GET article:view:123这样就能获取阅读量了。
如果要一次加N比如加10可以用incrby:
INCRBY article:view:123 10如果要减用decrdecrby。
进阶用法:
- 按时间统计: 如果需要按天小时统计比如每天的访问量每小时的访问量可以用不同的key比如site:view:20170306(今日)site:view:2017030610(今日10点)这样就能分别统计不同时间维度的数据还能用这些数据做趋势图等等。
- 限速限流: 计数器也能用来做限流比如限制某个用户每分钟最多访问100次可以用一个带过期时间的计数器key是rate:user1:201703061015(精确到分钟)每次访问incr一下如果超过100就拒绝因为key有过期时间下一分钟就重新计数了这就是简单的固定窗口限流当然更复杂的滑动窗口限流令牌桶漏桶也能用Redis实现。
- ID生成器: incr还能用来做ID生成器因为incr是原子的每次加1返回最新的值所以能生成唯一的递增ID比如订单号用户ID等等key是order:id每次incr就能得到一个唯一的订单ID很方便性能也很高。
注意事项:
- 数据持久化: 计数器的数据是在Redis里的如果Redis挂了数据可能会丢如果数据很重要比如文章阅读量不能丢要做好持久化或者定期把数据同步到数据库里比如每隔5分钟把Redis里的阅读量更新到MySQL里这样即使Redis挂了也不会丢太多。
- key的数量: 如果计数器的key特别多比如每篇文章一个key有几千万篇文章就要注意内存占用可以考虑用Hash把多篇文章的阅读量存在一个Hash里减少key的数量或者只在Redis里存热门文章的阅读量冷门文章的直接存数据库。
- 并发没问题: incr/decr是原子的所以并发更新没问题不用担心并发问题。
六、社交关系
社交系统里经常需要管理用户的关注粉丝好友等等Redis的Set(集合)很适合做社交关系因为Set支持交集并集差集能快速计算共同关注共同好友等等。
实现方式:
比如关注关系用户A关注了用户B我们用两个Set一个存用户的关注列表一个存用户的粉丝列表。
关注的key:follow:userA存userA关注的人。 粉丝的key:fans:userB存userB的粉丝。
用户A关注用户B:
SADD follow:userA userB
SADD fans:userB userA这两个要一起做保证一致性可以用事务或者Lua脚本。
取消关注:
SREM follow:userA userB
SREM fans:userB userA获取用户A的关注列表:
SMEMBERS follow:userA获取用户A的粉丝列表:
SMEMBERS fans:userA判断用户A是否关注了用户B:
SISMEMBER follow:userA userB获取用户A的关注数:
SCARD follow:userA获取用户A的粉丝数:
SCARD fans:userA获取用户A和用户C的共同关注:
SINTER follow:userA follow:userC这样就能快速得到两个人的共同关注很方便。
获取用户A可能认识的人(用户A关注的人关注的但是用户A没关注的):
SDIFF (SUNION follow:userB follow:userC) follow:userA这里userB和userC是userA关注的人先算出他们关注的人的并集再减去userA已经关注的就是userA可能认识的人。
是不是很强大?
注意事项:
- 数据量大的问题: 如果用户的关注/粉丝特别多比如大V有几千万粉丝Set的内存占用会很大而且smembers会一次性返回所有元素可能会阻塞Redis这时候不要用smembers要用sscan分批遍历或者只存前N个更多的存数据库。
- 一致性: 关注和粉丝是两个Set要保证一致性比如关注的时候两个Set都要加取消的时候都要删最好用事务或者Lua脚本保证原子性避免只改了一个另一个没改导致不一致。
- 持久化: 社交关系的数据很重要不能丢要做好持久化或者同时存一份到数据库里Redis作为缓存和加速查询。
七、最近列表
最近列表也是很常用的比如用户的最近浏览记录最近搜索记录最近播放记录等等Redis的List(列表)很适合做最近列表因为List能快速从头部添加能快速获取前N个还能限制列表的长度。
实现方式:
比如用户的最近浏览记录每个用户一个Listkey是recent:view:user1。
用户浏览了商品123:
LPUSH recent:view:user1 123
LTRIM recent:view:user1 0 99这里lpush把商品ID加到列表头部ltrim把列表只保留前100个也就是最近的100条浏览记录这样列表就不会无限增长占用太多内存。
获取用户的最近10条浏览记录:
LRANGE recent:view:user1 0 9这样就能获取最近的10条按时间倒序最新的在前面。
如果要去重也就是同一个商品多次浏览只保留最新的一次可以先把原来的删掉再加到头部:
LREM recent:view:user1 0 123
LPUSH recent:view:user1 123
LTRIM recent:view:user1 0 99这里lrem把列表里的123都删掉然后再lpush加到头部这样就去重了而且最新的在前面。
注意事项:
- 列表长度要限制: 一定要用ltrim限制列表的长度不然列表会无限增长占用大量内存一般保留最近的几十到几百条就够了。
- 去重的性能: 如果要去重用lrem+lpushlrem的时间复杂度是O(n)如果列表很长会慢一些不过一般最近列表都不会太长几十到几百条没问题如果特别长可以考虑用Sorted Set或者Set+List组合。
- 持久化: 最近列表的数据一般不是特别重要丢了也没关系所以不用太担心持久化的问题当然如果重要也可以存数据库。
八、其他高级用法
除了上面讲的这些Redis还有很多高级用法简单介绍一下:
- Bitmap(位图): Bitmap是String类型的一种特殊用法能存位信息适合做用户签到在线状态布隆过滤器等等比如用户签到key是sign:user1:201703每天一位签到了设为1没签到0这样一个月的签到只需要几个字节很省空间还能快速统计签到天数连续签到天数等等。
- HyperLogLog: HyperLogLog是一种基数统计算法能用很少的内存统计大量数据的去重数量比如统计网站的UV(独立访客)如果用Set存所有用户ID会占用大量内存而用HyperLogLog只需要12KB就能统计几十亿的UV虽然有一点误差大概0.81%但是对于大部分统计场景完全够用很省内存。
- Geo(地理位置): Redis 3.2引入了Geo功能能存地理位置信息快速计算两个位置的距离快速查找某个位置附近的其他位置适合做附近的人附近的店距离计算等等比如外卖的附近商家打车的附近司机社交的附近的人都能用Geo实现。
- 发布订阅: 前面讲消息队列的时候提到了Pub/Sub除了做消息队列还能做实时通知广播等等比如聊天室实时消息推送都能用。
九、最佳实践和踩坑经验
最后分享一些Redis的最佳实践和踩坑经验:
- 不要把Redis当数据库用: Redis是内存数据库虽然有持久化但是还是主要用来做缓存和加速不要把所有数据都存在Redis里重要的数据还是要存关系型数据库比如MySQLRedis作为缓存和辅助。
- key的命名要规范: key的命名要规范有意义比如用冒号分隔模块:功能:ID比如article:view:123user:info:456这样清晰易读也方便管理不要用太简单的key比如abc也不要太长浪费内存。
- 要设置过期时间: 除了一些需要永久保存的数据其他的key最好都设置过期时间避免key越来越多占用大量内存导致内存不足。
- 不要用keys命令: keys命令会遍历所有key时间复杂度是O(n)如果key很多会阻塞Redis影响性能生产环境不要用keys如果需要查找key用scan命令分批遍历不会阻塞。
- 大key要拆分: 如果一个key对应的value特别大比如一个List有几百万元素一个Hash有几百万字段会影响Redis的性能因为操作大key会耗时阻塞Redis而且迁移删除也很慢所以大key要拆分比如把一个大Hash拆成多个小Hash按ID取模分片。
- 要做好持久化: 如果Redis里的数据比较重要不能丢要做好持久化Redis有两种持久化方式RDB和AOFRDB是快照定期保存AOF是追加每次写操作都记录一般两种一起用既保证性能又保证数据安全同时要定期备份持久化文件。
- 要做好监控: Redis的监控很重要要监控内存使用CPU使用命中率连接数QPS等等及时发现问题比如内存快满了命中率下降了连接数太多了等等及时处理避免出故障。
- 要考虑高可用: 如果Redis是系统的关键组件不能挂要考虑高可用比如主从复制哨兵集群等等避免单点故障保证系统的可用性。
十、写在最后
Redis高级用法:不只是缓存这些场景你用过吗。
Redis真的很强大远不止缓存分布式锁消息队列排行榜计数器社交关系最近列表位图HyperLogLogGeo等等很多场景都能用而且性能很高用好了能大大提升系统的性能和开发效率。
当然Redis也不是银弹不是什么场景都适合要根据实际情况选择合适的方案同时要注意Redis的最佳实践避免踩坑。
希望这篇分享能帮大家更深入地了解Redis用好Redis少踩坑。
最后用一句话结尾:
"Redis不只是缓存它是一个多功能的数据结构服务器用好它能让你的系统更快更稳更高效。"
祝大家都能用好Redis让系统越来越快越来越稳!
评论(0)
暂无评论,快来抢沙发~
评论功能仅对会员开放,请先登录
登录