1、Redis的单/多线程
1.1、单线程
其实直接说Redis什么单线程或者是多线程,不太准确,在redis的4.0版主之前是单线程,然后在之后的版本中redis的渐渐改为多线程。
Redis是单线程主要是指Redis的网络IO和键值对读写是由一个线程来完成的,Redis在处理客户端的请求时包括获取 (socket 读)、解析、执行、内容返回 (socket 写) 等都由一个顺序串行的主线程处理,这就是所谓的“单线程”。这也是Redis对外提供键值存储服务的主要流程。
但Redis的其他功能,比如持久化RDB、AOF、异步删除、集群数据同步等等,其实是由额外的线程执行的。Redis命令工作线程是单线程的,但是,整个Redis来说,是多线程的。
那么问题来,Redis之前是单线程为什么还可以做到性能这么的好,主要是有四个方面,
-
首先是redis是基于内存的方式,Redis 的所有数据都存在内存中,因此所有的运算都是内存级别的,所以他的性能比较高;
-
其次数据结构简单:Redis 的数据结构是专门设计的,而这些简单的数据结构的查找和操作的时间大部分复杂度都是 O(1),因此性能比较高;
-
再者多路复用和非阻塞 I/O:Redis使用 I/O多路复用功能来监听多个 socket连接客户端,这样就可以使用一个线程连接来处理多个请求,减少线程切换带来的开销,同时也避免了 I/O 阻塞操作;
-
最后避免上下文切换:因为是单线程模型,因此就避免了不必要的上下文切换和多线程竞争,这就省去了多线程切换带来的时间和性能上的消耗,而且单线程不会导致死锁问题的发生。
在之前的版本中使用单线程模型是 Redis 的开发和维护更简单,因为单线程模型方便开发和调试;即使使用单线程模型也并发的处理多客户端的请求,主要使用的是IO多路复用和非阻塞IO;对于Redis系统来说,主要的性能瓶颈是内存或者网络带宽而并非 CPU。
但是随着时代的发展,cup也步入多核的时代,不改变就会被淘汰!当然对于单线程Redis也有一些小问题,比如说正常情况下使用 del 指令可以很快的删除数据,而当被删除的 key 是一个非常大的对象时,例如时包含了成千上万个元素的 hash 集合时,那么 del 指令就会造成 Redis 主线程卡顿。
于是在 Redis 4.0 中就新增了多线程的模块,当然此版本中的多线程主要是为了解决删除数据效率比较低的问题的。就是让子线程去处理这些问题,然后主线程正常的工作。
-
unlink key 惰性删除
-
flushdb async 异步清空数据库
-
flushall async 异步清空所有数据库
1.2、多线程
在Redis6/7中,非常受关注的第一个新特性就是多线程。这是因为,Redis一直被大家熟知的就是它的单线程架构,虽然有些命令操作可以用后台线程或子进程执行(比如数据删除、快照生成、AOF重写)。但是,从网络IO处理到实际的读写命令处理,都是由单个线程完成的。
随着网络硬件的性能提升,Redis的性能瓶颈有时会出现在网络IO的处理上,也就是说,单个主线程处理网络请求的速度跟不上底层网络硬件的速度,为了应对这个问题:采用多个IO线程来处理网络请求,提高网络请求处理的并行度,Redis6/7就是采用的这种方法。
但是,Redis的多IO线程只是用来处理网络请求的,对于读写操作命令Redis仍然使用单线程来处理。这是因为,Redis处理请求时,网络处理经常是瓶颈,通过多个IO线程并行处理网络操作,可以提升实例的整体处理性能。而继续使用单线程执行命令操作,就不用为了保证Lua脚本、事务的原子性,额外开发多线程互斥加锁机制了(不管加锁操作处理),这样一来,Redis线程模型实现就简单了
在Redis6.0及7后,多线程机制默认是关闭的,如果需要使用多线程功能,需要在redis.conf中完成两个设置。
四核给两个或者三个、八核的话给6个
io-threads 4
#配置项为yes,表示多线程的启用。
io-threads-do-reads no
1.2.1、主线程和io线程的工作方式
阶段一: 服务端和客户端建立Socket连接,并分配处理线程
首先,主线程负责接收建立连接请求,当有客户端请求和实例建立Socket连接时,主线程会创建和客户端的连接,并把Socket 放入全局等待队列中。紧接着,主线程通过轮询方法把Socket连接分配给IO线程。
阶段二:10线程读取并解析请求
主线程一旦把Socket分配给IO线程,就会进入阻塞状态,等待IO线程完成客户端请求读取和解析。因为有多个IO线程在并行处理,所以,这个过程很快就可以完成。
阶段三:主线程执行请求操作
等到IO线程解析完请求,主线程还是会以单线程的方式执行这些命令操作。
阶段四:10线程回写Socket和主线程清空全局队列
当主线程执行完请求操作后,会把需要返回的结果写入缓冲区,然后,主线程会阻塞等待IO线程,把这些结果回写到Socket中,并返回给客户端。和I0线程读取和解析请求一样,10线程回写Socket时,也是有多个线程在并发执行,所以回写Socket的速度也很快。等到I0线程回写Socket完毕,主线程会清空全局队列,等待客户端的后续请求。
1.2.2、五种网络编程中的IO模型
1.2.2.1、Blocking IO 阻塞IO
1.2.2.2、NoneBlocking IO 非阻塞IO
1.2.2.3、IO multiplexing IO多路复用
一种同步的IO模型,实现一个线程监视多个文件句柄,一旦某个文件句柄就绪就能够通知到对应应用程序进行相应的读写操作,没有文件句柄就绪时就会阻塞应用程序,从而释放CPU资源。
这里的句柄是指文件描述符(FileDescriptor)简称句柄或者FD, 文件描述符(File descriptor)是计算机科学中的一个术语,是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数。实际上,它是一个索引值,指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时,内核向进程返回一个文件描述符。在程序设计中,文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。
网络IO就是指操作系统层面指数据在内核态和用户态之间的读写操作。多路是指多个客户端的连接。复用是指复用一个或者多个线程。简单的说就是一个或一组线程处理多个TCP连接,使用单进程就能够实现同时处理多个客户端的连接,无需创建或者维护过多的进程/线程。一句话就是一个服务端进程可以同时处理多个套接字描述符。实现IO多路复用的模型有3种:可以分select->poll->epoll三个阶段来描述。
将用户socket对应的文件描述符(FileDescriptor)注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式。这样,整个过程只在调用select、poll、epoll这些调用的时候才会阻塞,收发客户消息是不会阻塞的,整个进程或者线程就被充分利用起来,这就是事件驱动,所谓的reactor反应模式。
在单个线程通过记录跟踪每一个Sockek(I/O流)的状态来同时管理多个I/O流. 一个服务端进程可以同时处理多个套接字描述符。目的是尽量多的提高服务器的吞吐能力。这就是IO多路复用原理,有请求就响应,没请求不打扰。
1.2.2.4、signal driven IO 信号驱动IO
1.2.2.5、asynchronous IO 异步IO
2、BigKey问题
2.1、keys * /flushdb/flushall
key * 这个指令有致命的弊端,在实际环境中最好不要使用,这个指令没有 offset、limit 参数,是要一次性吐出所有满足条件的 key,由于 redis 是单线程的,其所有操作都是原子的,而 keys 算法是遍历算法,复杂度是 0(n),如果实例中有千万级以上的 key,这个指令就会导致 Redis 服务卡顿,所有读写 Redis 的其它的指令都会被延后甚至会超时报错,可能会引起缓存雪崩甚至数据库宕机。
对于flushdb和flushall这种删库的操作,执行起来可简简单单,很刑!这些命令的使用的话应该在生产环境中做出一定的限制,可以在配置中设置禁用这些危险命令
rename-command keys ""
rename-command flushall ""
rename-command flushdb ""
经过这样操作后,重新执行这些限制的数据那么就会出现报错!所以这样设置可以避免生产事故。另外如果真的是一不小心的执行了清空数据库的操作,那么这时候如果使用的AOF的持久化操作,在没有重启redis主机之前(也就是再次重启恢复配置文件),可以编辑AOF文件,然后删除误操作的命令保存后重启redis也可以恢复数据。
但是除了keys *还可以使用其他什么命令来进行数据的匹配,可以使用scan
语法:SCAN cursor [MATCH pattern] [COUNT count]
-
cursor 游标
-
pattern 匹配的模式
-
count 指定从数据集中返回多少个元素,默认值是10
SCAN 命令是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为 SCAN 命令的游标参数, 以此来延续之前的迭代过程。
SCAN 返回一个包含两个元素的数组,
- 第一个元素是用于进行下一次迭代的新游标,
- 第二个元素则是一个数组, 这个数组中包含了所有被迭代的元素。如果新游标返回零表示迭代已结束。
SCAN的遍历顺序
非常特别,它不是从第一维数组的第零位一直遍历到末尾,而是采用了高位进位加法来遍历。之所以使用这样特殊的方式进行遍历,是考虑到字典的扩容和缩容时避免槽位的遍历重复和遗漏。
2.2、多大算BigKey
-
String类型控制在10kb以内
-
hash、list、set、zset元素的个数不要超过5000个
非字符串的bigkey,不要使用del删除使用hscan、sscan、zscan方式渐进式删除,同时要注意防止bigkey过期时间自动删除问题(例如一个200万的zset设置1小时过期,会触发del操作,造成阻塞,而且该操作不会出现在慢查询中(latency可查)),
如果key过大会导致内存分布不均匀、集群迁移困难,超时删除、网络流量阻塞等问题。
2.3、查找BigKey
redis-cli -h 127.0.0.1 -p 6379 -a 111111 --bigkeys
# Scanning the entire keyspace to find biggest keys as well as
# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec
# per 100 SCAN commands (not usually needed).
[00.00%] Biggest zset found so far 'shop:geo:2' with 5 members
[00.00%] Biggest set found so far 'follows:1024' with 2 members
[00.00%] Biggest string found so far 'icr:order:2024:01:16' with 1 bytes
[00.00%] Biggest set found so far 'setmealPicResources' with 7 members
[00.00%] Biggest zset found so far 'shop:geo:1' with 9 members
[00.00%] Biggest string found so far 'seckill:stock:20' with 2 bytes
-------- summary -------
Sampled 12 keys in the keyspace!
Total key length in bytes is 170 (avg len 14.17)
Biggest string found 'seckill:stock:20' has 2 bytes
Biggest set found 'setmealPicResources' has 7 members
Biggest zset found 'shop:geo:1' has 9 members
0 lists with 0 items (00.00% of keys, avg size 0.00)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
2 strings with 3 bytes (16.67% of keys, avg size 1.50)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
5 sets with 14 members (41.67% of keys, avg size 2.80)
5 zsets with 19 members (41.67% of keys, avg size 3.80)
语法:MEMORY USAGE key [SAMPLES count]
MEMORY USAGE 命令给出一个 key 和它的值在 RAM 中所占用的字节数返回的结果是 key 的值以及为管理该 key 分配的内存总字节数对于嵌套数据类型,可以使用选项 SAIIPLES,其中 count 表示抽样的元素个数,默认值为 5。当需要抽样所有元素时,使用 SAJPLES O 。
> Windows connected!
> MEMORY USAGE redis:category:
3270
2.4、BigKey的删除
-
string类型 一般使用del,如果过于庞大的话使用unlink
-
hash类型 渐进式删除,使用hscan每次获取少量field-value,再使用hdel删除每个field
-
list类型 使用ltrim渐进式逐步删除,直到全部删除完成
-
set类型 使用sscan每次获取部分元素,再使用srem命令删除每个元素
-
zset类型 使用zscan每次获取部分元素,再使用ZREMRANGEBYRANK命令删除每个元素
当然使用非阻塞性的删除也是必要的,这种方式可以优化redis的删除效率,可以在配置文件中设置
lazyfree-lazy-server-del no改为yes
replica-lazy-flush no改为yes
lazyfree-lazy-user-del no改为yes
3、缓存双写一致性
3.1、数据的更新策略
3.1.1、先更新数据库,后更新缓存
例子:先更新mysql的某商品的库存,当前商品的库存是100,更新为99个。先更新mysql修改为99成功,然后更新redis。此时假设异常出现,更新redis失败了,这导致mysql里面的库存是99而redis里面的还是100 。上述发生,会让数据库里面和缓存redis里面数据不一致,读到redis脏数据
例子:【先更新数据库,再更新缓存】,A、B两个线程发起调用
【正常逻辑】
1 A update mysql 100
2 A update redis 100
3 B update mysql 80
4 B update redis 80
【异常逻辑】多线程环境下,A、B两个线程有快有慢,有前有后有并行
1 A update mysql 100
3 B update mysql 80
4 B update redis 80
2 A update redis 100
最终结果,mysql和redis数据不一致,o(╥﹏╥)o,mysql80,redis100
3.1.2、先更新缓存,后更新数据库
例子:【先更新缓存,再更新数据库】,A、B两个线程发起调用
【正常逻辑】
1 A update redis 100
2 A update mysql 100
3 B update redis 80
4 B update mysql 80
【异常逻辑】多线程环境下,A、B两个线程有快有慢有并行
A update redis 100
B update redis 80
B update mysql 80
A update mysql 100
----mysql100,redis80
3.1.3、先删除缓存,再更新数据库
例子:
1、 A线程先成功删除了redis里面的数据,然后去更新mysql,此时mysql正在更新中,还没有结束。(比如网络延时)B突然出现要来读取缓存数据。
2 、此时redis里面的数据是空的,B线程来读取,先去读redis里数据(已经被A线程delete掉了),此处出来2个问题:
2.1 B从mysql获得了旧值,B线程发现redis里没有(缓存缺失)马上去mysql里面读取,从数据库里面读取来的是旧值。
2.2 B会把获得的旧值写回redis获得旧值数据后返回前台并回写进redis(刚被A线程删除的旧数据有极大可能又被写回了)。
3、A线程更新完mysql,发现redis里面的缓存是脏数据,A线程直接懵逼了,o(╥﹏╥)o
两个并发操作,一个是更新操作,另一个是查询操作,A删除缓存后,B查询操作没有命中缓存,B先把老数据读出来后放到缓存中,然后A更新操作更新了数据库。于是,在缓存中的数据还是老的数据,导致缓存中的数据是脏的,而且还一直这样脏下去了。
解决方法:
采用延迟双删的策略,上面的问题就是A线程执行删除后然后到数据库中更新缓存,但是这时候由于并发操作,来啦一个线程B那么这时候首先执行读取redsi,但是缓存中没有数据,然后回到数据库中查询数据,但是A还没有更新完毕数,那么此时B读取的数据还是之前的数据,然后由于回写机制,那么B会把读取到的数据写回redis,到头来A只更新了数据库中的数据但是对于redis中的数据是无效更新。
延时双删是指在A线程执行完删除Redis缓存后,到数据库中更新数据,在数据更换新完毕之后,将线程睡一会,睡一会的目的是为了避免此时有其他的线程读mysql中的数据后回写到redis中,当睡眠完成之后A线程再次执行一次删除缓存的操作。
其实这种方式在高并发的环境中也不是100%的有效,而且对于睡眠的设置也不太好掌握。当然也可以改变一种方式让其二次删除,可以将随眠转换为开一个子线程来处理删除缓存的操作。
3.1.4、先更新数据库,再删除缓存
例子:A更新数据库中的数据,然后删除缓存中的数据,这是理想的情况但是对于假如缓存删除失败或者来不及,导致请求再次访问redis时缓存命中,读取到的是缓存旧值。
3.2、canal
canal [kə'næl],译意为水道/管道/沟渠,主要用途是基于 MySQL,数据库增量日志解析,提供增量数据订阅和消费。https://github.com/alibaba/canal/releases/tag/canal-1.1.6
-
数据库镜像
-
数据库实时备份
-
索引构建和实时维护(拆分异构索引,倒排索引)
-
业务cache刷新
-
带业逻辑的增量数据处理
3.2.1、传统的mysql主从复制
MySQL的主从复制将经过如下步骤:
-
1、当 master 主服务器上的数据发生改变时,则将其改变写入二进制事件日志文件中;
-
2、salve 从服务器会在一定时间间隔内对 master 主服务器上的二进制日志进行探测,探测其是否发生过改变,(offset偏移量)
-
如果探测到 master 主服务器的二进制事件日志发生了改变,则开始一个 I/O Thread 请求 master 二进制事件日志;
-
3、同时 master 主服务器为每个 I/O Thread 启动一个dump Thread,用于向其发送二进制事件日志;
-
4、slave 从服务器将接收到的二进制事件日志保存至自己本地的中继日志文件中;
-
5、salve 从服务器将启动 SQL Thread 从中继日志中读取二进制日志,在本地重放,使得其数据和主服务器保持一致;
-
6、最后 I/O Thread 和 SQL Thread 将进入睡眠状态,等待下一次被唤醒;
3.2.2、canal工作原理
工作原理
-
canal 模拟 MySQL slave 的交互协议,伪装自己为 MysQL slave,向 MySQL master 发送 dump 协议。
-
MySQL master 收到 dump 请求,开始推送 binary log 给 slave (即 canal)
-
canal 解析 binary log 对象(原始为 byte 流)
3.2.3、mysql配置
[mysqld]
log-bin=mysql-bin #开启
binlogbinlog-format=ROW #选择 ROW 模式
server_id=1 #配置MySQL replaction需要定义,不要和canal的 slaveId重复
ROW模式 除了记录sql语句之外,还会记录每个字段的变化情况,能够清楚的记录每行数据的变化历史,但会占用较多的空间。STATEMENT模式只记录了sql语句,但是没有记录上下文信息,在进行数据恢复的时候可能会导致数据的丢失情况;MIX模式比较灵活的记录,理论上说当遇到了表结构变更的时候,就会记录为statement模式。当遇到了数据更新或
授权canal连接mysql账号
DROP USER IF EXISTS 'canal'@'%';
CREATE USER 'canal'@'%' IDENTIFIED BY 'canal';
GRANT ALL PRIVILEGES ON *.* TO 'canal'@'%' IDENTIFIED BY 'canal';
FLUSH PRIVILEGES;
SELECT * FROM mysql.user;
3.2.4、canal安装配置
首先将下载好的文件上传到Linux系统中,然后在指定的目录下解压
tar -zxvf canal.deployer-1.1.7.tar.gz
修改配置文件
cd conf/example
vim instance.properties
#修改主机的mysql地址
canal.instance.master.address=192.168.200.66:3306
canal.instance.master.journal.name=
canal.instance.master.position=
canal.instance.master.timestamp=
canal.instance.master.gtid=
#连的的canal用户名和密码
canal.instance.dbUsername=canal
canal.instance.dbPassword=canal
canal.instance.connectionCharset = UTF-8
# enable druid Decrypt database password
canal.instance.enableDruid=false
启动
#切换到bin目录下
./startup.sh
查看日志
切换到日志文件夹
cat canal.log
cat example.log
4、特殊数据结构的应用
4.1、数据统计的类型
4.1.1、聚合统计
统计多个集合元素的聚合的结果,实际上就是使用集合的交并差的运算规则来进行数据的统计。
4.1.2、排序统计
统计评论列表,排行榜的应用话,可以使用zset,zset的数据结构有序不重复,
4.1.3、二值统计
使用bitmap可以解决类似每日签到,以及上班打卡的业务。使用bitmap可以节省数据的存储空间,优化存储结构。
4.1.4、基数统计
统计一个集合中不重复的元素的个数,而且这种统计结果不存储,只用做统计!
4.2、hyperloglog
4.2.1、行话
-
UV unique visitor(独立访客)一般为客户端的IP,需要去重考虑
-
PV Page View(页面浏览量)不用去重
-
DAU Daily Active User (日活跃用户量)登录或者使用某一个产品的用户数(去除重复的用户)
-
MAU Monthly Active User (月活跃用户量)
很多计数类场景,比如 每日注册 IP 数、每日访问 IP 数、页面实时访问数 PV、访问用户数 UV等。因为主要的目标高效、巨量地进行计数,所以对存储的数据的内容并不太关心。也就是说它只能用于统计巨量数量,不太涉及具体的统计对象的内容和精准性。
统计单日一个页面的访问量(PV),单次访问就算一次。统计单日一个页面的用户访问量(UV),即按照用户为维度计算,单个用户一天内多次访问也只算一次。多个key的合并统计,某个门户网站的所有模块的PV聚合统计就是整个网站的总PV。
4.2.2、使用
HyperLogLog就是一种概率算法的实现,通过牺牲准确率来换取空间,对于不要求绝对准确率的场景下可以使用,因为概率算法不直接存储数据本身,
通过一定的概率统计方法预估基数值,同时保证误差在一定范围内,由于又不储存数据故此可以大大节约内存。只是进行不重复的基数统计,不是集合也不保存数据,只记录数量而不是具体内容。
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
* @auther zzyy
* @create 2021-05-02 18:16
*/
@Service
@Slf4j
public class HyperLogLogService
{
@Resource
private RedisTemplate redisTemplate;
/**
* 模拟后台有用户点击首页,每个用户来自不同ip地址
*/
@PostConstruct
public void init()
{
log.info("------模拟后台有用户点击首页,每个用户来自不同ip地址");
new Thread(() -> {
String ip = null;
for (int i = 1; i <=200; i++) {
Random r = new Random();
ip = r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256) + "." + r.nextInt(256);
Long hll = redisTemplate.opsForHyperLogLog().add("hll", ip);
log.info("ip={},该ip地址访问首页的次数={}",ip,hll);
//暂停几秒钟线程
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); }
}
},"t1").start();
}
}
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @auther zzyy
* @create 2021-05-02 18:16
*/
@Api(description = "淘宝亿级UV的Redis统计方案")
@RestController
@Slf4j
public class HyperLogLogController
{
@Resource
private RedisTemplate redisTemplate;
@ApiOperation("获得IP去重后的首页访问量")
@RequestMapping(value = "/uv",method = RequestMethod.GET)
public long uv()
{
//pfcount
return redisTemplate.opsForHyperLogLog().size("hll");
}
}
4.3、GEO
4.3.1、经纬度
经纬度
经度与纬度的合称组成一个坐标系统。又称为地理坐标系统,它是一种利用三度空间的球面来定义地球上的空间的球面坐标系统,能够标示地球上的任何一个位置。
经线和纬线
是人们为了在地球上确定位置和方向的,在地球仪和地图上画出来的,地面上并线和经线相垂直的线叫做纬线(纬线指示东西方向)。纬线是一条条长度不等的圆圈。最长的纬线就是赤道。
因为经线指示南北方向,所以经线又叫子午线。 国际上规定,把通过英国格林尼治天文台原址的经线叫做0°所以经线也叫本初子午线。在地球上经线指示南北方向,纬线指示东西方向。东西半球分界线:东经160° 西经20°
经度和维度
-
经度(longitude):东经为正数,西经为负数。东西经
-
纬度(latitude):北纬为正数,南纬为负数。南北纬
4.3.2、使用
import com.atguigu.redis7.service.GeoService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @auther zzyy
* @create 2022-12-25 12:12
*/
@Api(tags = "美团地图位置附近的酒店推送GEO")
@RestController
@Slf4j
public class GeoController
{
@Resource
private GeoService geoService;
@ApiOperation("添加坐标geoadd")
@RequestMapping(value = "/geoadd",method = RequestMethod.GET)
public String geoAdd()
{
return geoService.geoAdd();
}
@ApiOperation("获取经纬度坐标geopos")
@RequestMapping(value = "/geopos",method = RequestMethod.GET)
public Point position(String member)
{
return geoService.position(member);
}
@ApiOperation("获取经纬度生成的base32编码值geohash")
@RequestMapping(value = "/geohash",method = RequestMethod.GET)
public String hash(String member)
{
return geoService.hash(member);
}
@ApiOperation("获取两个给定位置之间的距离")
@RequestMapping(value = "/geodist",method = RequestMethod.GET)
public Distance distance(String member1, String member2)
{
return geoService.distance(member1,member2);
}
@ApiOperation("通过经度纬度查找北京王府井附近的")
@RequestMapping(value = "/georadius",method = RequestMethod.GET)
public GeoResults radiusByxy()
{
return geoService.radiusByxy();
}
@ApiOperation("通过地方查找附近,本例写死天安门作为地址")
@RequestMapping(value = "/georadiusByMember",method = RequestMethod.GET)
public GeoResults radiusByMember()
{
return geoService.radiusByMember();
}
}
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.geo.Distance;
import org.springframework.data.geo.GeoResults;
import org.springframework.data.geo.Metrics;
import org.springframework.data.geo.Point;
import org.springframework.data.geo.Circle;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @auther zzyy
* @create 2022-12-25 12:11
*/
@Service
@Slf4j
public class GeoService
{
public static final String CITY ="city";
@Autowired
private RedisTemplate redisTemplate;
public String geoAdd()
{
Map<String, Point> map= new HashMap<>();
map.put("天安门",new Point(116.403963,39.915119));
map.put("故宫",new Point(116.403414 ,39.924091));
map.put("长城" ,new Point(116.024067,40.362639));
redisTemplate.opsForGeo().add(CITY,map);
return map.toString();
}
public Point position(String member) {
//获取经纬度坐标
List<Point> list= this.redisTemplate.opsForGeo().position(CITY,member);
return list.get(0);
}
public String hash(String member) {
//geohash算法生成的base32编码值
List<String> list= this.redisTemplate.opsForGeo().hash(CITY,member);
return list.get(0);
}
public Distance distance(String member1, String member2) {
//获取两个给定位置之间的距离
Distance distance= this.redisTemplate.opsForGeo().distance(CITY,member1,member2, RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance;
}
public GeoResults radiusByxy() {
//通过经度,纬度查找附近的,北京王府井位置116.418017,39.914402
Circle circle = new Circle(116.418017, 39.914402, Metrics.KILOMETERS.getMultiplier());
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,circle, args);
return geoResults;
}
public GeoResults radiusByMember() {
//通过地方查找附近
String member="天安门";
//返回50条
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance().includeCoordinates().sortAscending().limit(50);
//半径10公里内
Distance distance=new Distance(10, Metrics.KILOMETERS);
GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults= this.redisTemplate.opsForGeo().radius(CITY,member, distance,args);
return geoResults;
}
}
4.4、BitMap
由0和1组成的二进制位的bit数组,可用于日活跃统计,连续的签到打卡等等