在日常开发里,Redis 的身影几乎无处不在。

有不少人在最初与它进行接触之际,皆是源于它具备的高性能,将其当作一种颇为简易便捷用作短暂保存的数据缓冲来运作,使用 Map去存储数据,速度既快且操作便利。

但业务变得复杂起来,单机缓存不够用了,于是开始思考它的更多玩法。在玩法中,有的内容是列表、有的内容是哈希、有的内容是有序集合这些结构,甚至还想着让它承担起数据库的角色不然就承担起消息队列的角色。

然而,理想极为丰满,现实常常很骨感,稍有不慎便会落入某些坑中,致使本应飞速的系统变得卡顿,乃至数据错乱。

列表不是数组,别拿它当数组用

127.0.0.1:6379> set a b
OK

许多人在初次碰到Redis列表之际,会不由自主地将其当作编程语言当中的数组看待,惯常用下标来对元素予以访问。

127.0.0.1:6379> get a
"b"

事实上,Redis列表乃是依据双向链表予以实现的,头部操作快捷方便,却在中间部位的访问上颇显迟缓,尾部操作亦是如此,快速便捷!

127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> get a
(nil)

127.0.0.1:6379> set a a
OK
127.0.0.1:6379> set a a nx
(nil)

要是在业务范畴內存在频繁地随机去访问处于中间位置的元素这种情况,恰似去构建这么一个评论列表,用户老是跳转到第五十页,那么采用列表这种方式就不太适宜了。

127.0.0.1:6379> set b b xx
(nil)
127.0.0.1:6379> set b b
OK
127.0.0.1:6379> set b c xx
OK
127.0.0.1:6379> get b
"c"

127.0.0.1:6379> get a
"a"
127.0.0.1:6379> incr a
(error) ERR value is not an integer or out of range
127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> incr a
(integer) 2

我从前碰到过朋友所做的项目,正是缘于那般情况,将接口响应时间从中的几十毫秒,拖延至好几秒,最终经由换成有序集合,才把问题给解决掉了。

127.0.0.1:6379> del a
(integer) 1
127.0.0.1:6379> incr a
(integer) 1

所以,在使用列表之前,务必要好好想明白,你的那个场景到底是顺序方面的访问会比较多,还是随机这一块的访问会比较多。

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> incrby a 3
(integer) 4

缓存穿透和过期时间,别偷懒不设

127.0.0.1:6379> set a 1
OK
127.0.0.1:6379> get a
"1"
127.0.0.1:6379> expire a 2
(integer) 1
127.0.0.1:6379> get a
(nil)

127.0.0.1:6379> set a 2 EX 3
OK
127.0.0.1:6379> get a
"2"
127.0.0.1:6379> get a
(nil)

在进行缓存操作期间,存在着一件极为容易被忽视的事情,那便是针对数据设定恰当合理的过期时间。

127.0.0.1:6379> getset a 2
(nil)
127.0.0.1:6379> getset a 3
"2"

127.0.0.1:6379> mset a 1 b 2
OK
127.0.0.1:6379> mget a b
1) "1"
2) "2"

有人持有这样的想法,那就是反正Redis内存大,所以数据存放在那里也不会出现什么问题,然而当时间持续久了之后,内存里面充斥的全部都是冷数据,由此导致性能呈现出直线下降的情况。

更麻烦的情况是,要是某个热点数据没有设置过期时间,然而却突然失效了,在那一瞬间,大量请求直接涌向数据库,如此一来特别容易把数据库弄崩溃。

另外存在这样一种情形,即缓存穿透;所查找的数据在数据库当中同样不存在;每一次发起请求都会略过缓存,直接去查找数据库。

sadd article:1000:tags 1 4 5 6 

针对此种情形来讲,能够将空结果予以缓存留存,设定一个相对短些的过期时段,像几分钟这样,便可以防止大量无实际效用的请求穿透而过。

sadd tag:1:article 1000
sadd tag:1:article 1001 

sinter tag:1:article tag:2:article ...

消息队列用列表,得考虑消费者挂了怎么办

将 Redis 列表用作消息队列的这种用法是颇为常见的,其中生产者是借助 LPUSH 来塞入消息的,而消费者则是通过 RPOP 去获取消息的。

127.0.0.1:6379> zadd test 2 a
(integer) 1

可是,这儿存在着一个隐蔽的风险,倘若消费者获取到消息之后,还没能够来得及展开处理,程序便出现了崩溃状况,那么,这条消息将会遗失。

127.0.0.1:6379> zadd test incr 2 a
"4"

Redis缓存数据库使用_Redis键值对存储_数据库Redis列表

要是想确保消息不会丢失,能够使用 BRPOPLPUSH 判断,它能够将消息从队列之中检索出来,与此同时存储到另外一种列表当中去,等到消费者处理完毕并确认无误之后,再从备份里面加以删除。

127.0.0.1:6379> zadd test 2 a
(integer) 1
127.0.0.1:6379> zadd test 3 b
(integer) 1
127.0.0.1:6379> zadd test 1 c
(integer) 1
127.0.0.1:6379> zadd test 4 d
(integer) 1
127.0.0.1:6379> zrange test 0 -1
1) "c"
2) "a"
3) "b"
4) "d"

127.0.0.1:6379> zrange test 0 1
1) "c"
2) "a"

这个操作,能够极大程度提高系统的可靠性,特别是在涉及订单、支付这类重要业务的情况下,更是绝对不能省略的。

127.0.0.1:6379> zrevrange test 0 1
1) "d"
2) "b"

127.0.0.1:6379> zrevrange test 0 1 withscores
1) "d"
2) "4"
3) "b"
4) "3"

哈希存对象,别一个键存所有字段

127.0.0.1:6379> zrangebyscore test -inf 2
1) "c"
2) "a"
127.0.0.1:6379> zrangebyscore test -inf 3
1) "c"
2) "a"
3) "b"
127.0.0.1:6379> zrangebyscore test 1 2
1) "c"
2) "a"

127.0.0.1:6379> zrank test a
(integer) 1
127.0.0.1:6379> zrank test b
(integer) 2
127.0.0.1:6379> zrank test c
(integer) 0

对于 Redis 而言,哈希去存储对象的属性,真的是十分便利,就好比,借助 HSET user:1001 name"张三" age 25,如此一来,便能够将用户的相关信息妥善保存好。

127.0.0.1:6379> del test
(integer) 1
127.0.0.1:6379> hset test a b
(integer) 1
127.0.0.1:6379> hget test a
"b"

可是呢,存在这样一些开发习惯,会运用一个哈希去存储所有用户的字段,比如说,呈现为 HSET users name"张三",name2"李四"这种形式,如此一来,就变得麻烦了呀。

127.0.0.1:6379> hset test c 1
(integer) 1
127.0.0.1:6379> hincrby test c 20
(integer) 21

当进行某个字段的查询操作的情况下,是势必要扫描整个哈希的,而要是对单个用户作出修改的行为时,同样要对这个规模较大的哈希实施操作,非常容易致使热点Key问题的出现。

127.0.0.1:6379> setbit test 0 1
(integer) 0
127.0.0.1:6379> setbit test 3 1
(integer) 0

将每个用户对应一个单独的哈希键,此为正确做法,它不但能够实现快速定位,而且可以独立去设置过期时间,并且扩展性表现得更为出色。

127.0.0.1:6379> getbit test 3
(integer) 1

127.0.0.1:6379> bitcount test
(integer) 2

有序集合做排行榜,分值更新要小心

127.0.0.1:6379> bitpos test 0
(integer) 1
127.0.0.1:6379> bitpos test 1
(integer) 0

127.0.0.1:6379> bitop and result test test1
(integer) 1

有序集合于排行榜场景当中,极其好用,举例而言,像是游戏积分榜,还有文章热度榜这样类别的。

有的朋友会直接借由ZADD去更新分值,但要是分值得进行累加,像用户每次操作都要加5分这种情况,ZINCRBY才是正确的选取,它能确保原子性,不会产生并发问题。

127.0.0.1:6379> setbit a 0 1
(integer) 0
127.0.0.1:6379> setbit a 3 1
(integer) 1
127.0.0.1:6379> setbit b 2 1
(integer) 1
127.0.0.1:6379> setbit b 3 1
(integer) 1
127.0.0.1:6379> setbit c 0 1
(integer) 0
127.0.0.1:6379> setbit c 3 1
(integer) 0
127.0.0.1:6379> setbit c 1 1
(integer) 0
127.0.0.1:6379> bitop and result a b c
(integer) 1

另外,要是排行榜的数据量十分庞大,当只选取Top 100的时候,要记住运用ZREVRANGE搭配WITHSCORES,千万别一口气统统取出来,然后又在代码里进行排序,如此一来既会浪费带宽,又会增加服务器的压力。

127.0.0.1:6379> del test
(integer) 1
127.0.0.1:6379> pfadd test a b c d a b
(integer) 1
127.0.0.1:6379> pfcount test
(integer) 4
127.0.0.1:6379> pfadd test e
(integer) 1
127.0.0.1:6379> pfcount test
(integer) 5

高可用配置,别只配一个哨兵

为了保障线上服务稳定,很多人会给Redis搭主从哨兵

可有个极易被人们给忽略忘记的要点,哨兵集群起码得去部署三个节点才行,这是由于主节点在进行下线判定层面,是需要开展投票操作的,在只有两个节点的情形状况下,假使要是它们之中有一个出现故障挂掉了,那么剩余下来的那一个节点,是根本没办法达成法定票数要求的,如此一来整个集群就没得办法去自动切换主节点了。

我曾目睹有公司仅仅配备了两名哨兵,然而在半夜时分,主节点出现宕机状况,致使哨兵无法进行投票,只能由人工实施介入,进而造成业务中断了差不多整整一个小时。

Redis缓存数据库使用_数据库Redis列表_Redis键值对存储

所以呀,高可用配置必须得依据官方所给出的推荐去进行操作,切勿因为只是着想节省资源,从而埋下隐患呢。

总得来讲,Redis尽管使用起来较为简便,然而要是想将其运用得恰到好处,依旧需要耗费精力去钻研每一个数据结构的底层原理以及适用场景。

public void executePipelined(Map map, long seconds) {
        RedisSerializer serializer = redisTemplate.getStringSerializer();
        redisTemplate.executePipelined(new RedisCallback() {
            @Override
            public String doInRedis(RedisConnection connection) throws DataAccessException {
                map.forEach((key, value) -> {
                    connection.set(serializer.serialize(key), serializer.serialize(value),Expiration.seconds(seconds), RedisStringCommands.SetOption.UPSERT);
                });
                return null;
            }
        },serializer);
    }

哈希、列表、有序集合,它们各自具备着不同的长处,要是能够在恰当的地方得以运用,那么性能将会实现翻倍增长,然而一旦用错了地方,极有可能就会变成系统的瓶颈所在。

对业务场景要多多去思考,对于底层机制要再多知晓一些,如此一来,好多坑实际上是能够预先避开的。