redis(2): 高级结构和基本用法

这是我参与更文挑战的第23天,活动详情查看: 更文挑战

前言

上篇文章介绍了redis的入门概念和基本数据结构,在本文中,我们将开始高级数据结构的学习,并看看redis这种还有除了做缓存外,还有哪些高级的应用?

一、三种特殊数据类型

1.1 Bigmaps –位图 (节衣缩食小能手)

位图的出现是为了在一些场景下节约存储空间而应用的, 在存储一些布尔值的时候,只有0 和1 两个状态,例如,用户的签到记录,如用用string 来存储的话,当上亿用户的时候,存储空间是惊人的。而位图其实就像一个byte 数组,每一个字节有8 bit,每个bit 只能存储0 和1 ,这样一年 365 天 用一个46字节的字符串就可以容纳下来。

基本使用:

###setbit getbit bitcount
# 打卡 只需去判断是否为1就可以
127.17.197.44:6379> setbit sign 0 1
(integer) 0
127.17.197.44:6379> setbit sign 1 0
(integer) 0
127.17.197.44:6379> setbit sign 2 0
(integer) 0
127.17.197.44:6379> setbit sign 3 1
(integer) 0
127.17.197.44:6379> setbit sign 4 0
(integer) 0
127.17.197.44:6379> setbit sign 5 1

127.0.0.1:6379[9]> get sign
"\x94"

127.17.197.44:6379> getbit sign 5
(integer) 1

127.17.197.44:6379> bitcount sign
(integer) 3
复制代码

这里的操作是我们一位一位的赋值,然后通过get 整体取出来,称为“零存整取”。 通过getbit 取出来 称为“零存零取” 。当然 通过字符串 set ,然后getbit 取出来称为 “整存零取”。

通过 查找 和 统计 。

  • bitcount : 统计这个值有多少个1
  • bitpos:查找指定范围内第一个出现的1或0 所在位数

1.2 Hyperloglog

1.2.1 概述

Redis 在2.8.9 版本添加了HyperLogLog 结构。它是用来做基数统计的算法,HyperLogLog 的优点是,在输入元素的数量或者体积非常大时,计算基数所需的空间总是固定的,并且是很小的。在redis中,每个HyperLogLog 键 只需要花费 12kb 内存 ,就可以计算接近2^ 64个不同的基数。 远远节省了内存,但是它只会根据元素来计算基数,而不会存储输入元素本身,所以HyperLogLog 不能像集合那样,返回输入的各个元素。

那么什么是基数呢?

比如,数据集{1,2,3,4,4,7,8,8,8} 基数集合(不重复的个数) {1,2,3,4,7,8} ,基数(基数集合大小)为6。

基数估计就是在误差可接收的范围内,快速计算基数。

1.2.2 应用场景

如果页面访问量非常大,比如 一个爆款页面可能有几千万个浏览量。需要统计浏览页面的总用户个数,就比较麻烦,需要用一个set 集合来存储用户id 。在影响性能的同时,也增大了存储空间。

HyperLogLog 数据结构 提供了不精确的去重计数方案,标准误差为0.81% 。

1.2.3 使用方法
  • pfadd :增加计数
  • pfcount:获取计数
  • pfmerge:将后面键的值加到前面键的值
127.0.0.1:6379[9]> pfadd web user1
(integer) 1
127.0.0.1:6379[9]> pfadd web user2
(integer) 1
127.0.0.1:6379[9]> pfadd web user3
(integer) 1
127.0.0.1:6379[9]> pfcount web
(integer) 3
127.0.0.1:6379[9]> pfadd web user4 user5
(integer) 1
127.0.0.1:6379[9]> pfadd web2 user6 user7
(integer) 1
127.0.0.1:6379[9]> pfcount web web2
(integer) 7
127.0.0.1:6379[9]> pfmerge web web2
OK
127.0.0.1:6379[9]> pfcount web web2
(integer) 7
127.0.0.1:6379[9]> pfcount web 
(integer) 7
127.0.0.1:6379[9]> pfcount web2
(integer) 2
复制代码
1.2.4 测试

向redis 中插入1w条数据测试下

public static void testHyperLogLog(){
        Jedis jedis = new Jedis("10.240.30.103", 6379);
        jedis.auth("***");
        jedis.select(9);
        for (int i = 0; i < 10000; i++) {
            jedis.pfadd("web","user"+i);
            if(i==9999){
                long total = jedis.pfcount("web");
                System.out.println("总个数:"+(i+1)+"实际个数:"+total);
            }
        }
    }
复制代码
总个数:10000实际个数:10067
复制代码

在1w的时候,误差率为0.67%。 小于0.81% 的标准。

1.3 geospatial 地理位置

  • 用于 地图定位,附近的人,打车距离计算等场景

  • GEO存储的数据结构是存放在一个zset集合中,建议使用单独的redis实例部署GEO数据。

  • 只有6个命令:

    • GEOADD :添加
    • GEODIST : 两个位置间距离
    • GEOHASH :返回一个或多个位置的geohash值
    • GEOPOS:获取当前定位
    • GEORADIUS :附近的人,通过半径
    • GEOGEORADIUSBYMEMBER:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合
  • 学习网站:

    添加

    127.0.0.1:6379> geoadd china:city 121.47 31.23 shanghai
    (integer) 1
    127.0.0.1:6379> geoadd china:city 120.16 30.24 hangzhou
    (integer) 1
    复制代码

    获取当前定位:

    127.0.0.1:6379> geopos china:city shanghai hangzhou
    1) 1) "121.47000163793563843"
       2) "31.22999903975783553"
    2) 1) "120.1600000262260437"
       2) "30.2400003229490224"
    复制代码

    两个位置间距离:单位 m ,km, mt,ft

    127.0.0.1:6379> geodist china:city shanghai hangzhou
    "166761.2770"
    127.0.0.1:6379> geodist china:city shanghai hangzhou km
    "166.7613"
    复制代码

    附近的人,通过半径来找:

    127.0.0.1:6379> georadius china:city 120 30 1000 km
    1) "hangzhou"
    2) "shanghai"
    
    # 限制一个
    127.0.0.1:6379> georadius china:city 120 30 1000 km withdist withcoord count 1
    1) 1) "hangzhou"
       2) "30.8146"
       3) 1) "120.1600000262260437"
          2) "30.2400003229490224"
    复制代码

二、高级应用

2.1 分布式锁

2.1.1 分布式锁概要

锁,贯穿着我们的学习过程,在并发编程中,我们常常通过加锁来避免由于线程竞争而造成是数据不一致问题,但是Java中的锁往往只能在一个JVM进程内执行,如果在分布式集群下就需要用到分布式锁。

基于分布式锁常用的实现方式有三种:

  • 基于数据库乐观锁实现分布式锁
  • 基于缓存redis等实现分布式锁
  • 基于zookeeper实现分布式锁

今天,我们要来说说redis 中是怎么使用分布式锁的。

2.1.2 redis实现分布式锁

基本命令:

127.0.0.1:6379[9]> setnx lock lei  # 在键不存在情况下,设置key 和value
(integer) 1
127.0.0.1:6379[9]> setnx lock lei2 # 在lock被锁住的时候,去访问它则返回0
(integer) 0
127.0.0.1:6379[9]> get lock # 验证lock 的值有没有被覆盖
"lei"
127.0.0.1:6379[9]> del lock # 删除锁
(integer) 1
127.0.0.1:6379[9]> set lock lei ex 20 nx  # 保证原子性,既加锁,又设过期时间
OK
复制代码

其中set 参数,可以保证加锁和过期时间的原子性,防止有时候加锁成功,却没有加过期时间。

参数如下:

set key value [EX seconds] [PX milliseconds] [NX|XX]
EX seconds:设置失效时长,单位秒
PX milliseconds:设置失效时长,单位毫秒
NX:key不存在时设置value,成功返回OK,失败返回(nil)
XX:key存在时设置value,成功返回OK,失败返回(nil)
复制代码
2.1.3 超时问题

Redis 分布式锁不能解决超时问题,也就是第一个线程加上锁之后,就开始进行后面的逻辑代码,但是在锁的时间到了之后,后面代码还没执行结束,另一个线程就来占有这把锁,导致代码不能正确执行。

因此,redis的分布式锁不能解决较长时间的任务。

解决的方式就是在value 上加个随机数,释放锁的时候先匹配随机数是否一致,然后再删除key,这是为了确保当前线程占有的锁不会被其他线程释放,除非过期了。

2.2 延时队列

redis 也是可以作为消息队列来使用的,与我们熟悉的rabbitmq 和kafka类似,作为异步消息传递。

但是redis 作为消息队列的特点是 它实现比较简单,但同时,它没有非常多的高级特性,没有ack 保证,对消息的可靠性没有极高要求时,可以考虑采用。

2.2.1 基于List的实现

redis 中的list 可以作为一个而消息队列,可以保证在队列的一端进行rpush 和lpush 操作入队列,用lpop 和rpop 操作出队列。不过考虑到如果队列中没有数据的时候,一直查询redis 会造成 cpu 性能消耗,可以采用 blpop / brpop 替代前面的rpop / lpop ,在没有数据的时候会进入休眠状态,一旦数据来了,就立即读取数据,延迟很低。

img

2.2.2 PUB/SUB ,发布订阅模式

Redis 的发布订阅是一种消息通信模式:发送者发送消息,订阅者接收消息。例如微博、微信、关注系统等。

Redis 客户端可以订阅任意数量的频道。也就是多个消费者都可以得到相同的消息。

SUBSCRIBE,用于订阅信道
PUBLISH,向信道发送消息
UNSUBSCRIBE,取消订阅
复制代码

优点:

  • 典型的广播模式,一个消息可以发布到多个消费者
  • 多信道订阅,消费者可以同时订阅多个信道,从而接收多类别消息
  • 消息实时发送,不用等待消费者读取,消费端自动接收。

缺点:

  • 若消息发布时,客户端不在线,则消息丢失,不能寻回。
  • 不能保证每个消费者接收的时间是一致的
  • 若消费者客户端出现消息积压,到一定程序,会被强制断开,导致消息意外丢失

因此,发布订阅更擅长处理广播,及时通讯,及时反馈的业务。

简单案例:

## 发布者
127.0.0.1:6379> publish xiaolei "hello ,xiaolei"
(integer) 1
## 订阅者
127.0.0.1:6379> SUBSCRIBE kuangshengshuo #订阅一个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) (integer) 1
#等待推送的信息
1) "message"
2) "hello ,xiaolei"
复制代码

使用场景:

  • 1、实时消息系统!
  • 2、事实聊天!(频道当做聊天室,将信息回显给所有人即可!)
  • 3、订阅,关注系统都是可以的!

2.3 redis 事务

  • redis 单条命令 是保存原子性的,但是事务不保证原子性。
  • redis 事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行。一次性、顺序性、排他性
  • redis 事务没有隔离级别的概念。
  • 所有的命令在事务中,并没有直接被执行,只有发起执行命令的时候才会执行。
  • redis 的事务:
    • 开启事务(multi)
    • 命令入队()
    • 执行事务(exec)
    • 放弃命令(discard)
127.0.0.1:6379> multi ## 开启事务
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> exec ## 执行事务
1) OK
2) OK
3) "v2"
4) OK
复制代码
127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1 set k2 v2
QUEUED
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> discard  ## 使用discard,事务队列中命令都不会被执行
OK
127.0.0.1:6379> get k4
(nil)
复制代码

编译期异常(代码有问题!命令有错),事务中的所有命令都不会被执行。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> getset k3
(error) ERR wrong number of arguments for 'getset' command
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> exec ##错误的命令,执行事务报错,所有命令都不会执行
(error) EXECABORT Transaction discarded because of previous errors.
复制代码

运行期异常(1/0),如果事务队列存在语法性,那么执行命令的时候,其他命令是可以正常执行的,错误命令抛出异常

127.0.0.1:6379> multi 
OK
127.0.0.1:6379> incr k1
QUEUED
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> exec ## 单条有原子性,整体没有
1) (error) ERR value is not an integer or out of range
2) OK
3) OK
127.0.0.1:6379> get k1
"v1"
复制代码

参考资料:

  • 《Redis 深度历险》
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享