物联网应用中实时定位与轨迹回放的解决方案 – Redis的典型运用(转载)
2015年11月14日| by: nbboy| Category: 系统设计, 缓存设计, 高性能系统
摘要 我们解决某个问题,很多时候并不在于你掌握了某个工具或某项技术,而在于你对该场景下该问题的本质理解。 这则博文,针对物联网应用中实时定位与轨迹回放的问题,阐述最为简要的解决之道。
目录[-]
1. 系统需求
2. 应用场景
3. 数据量估算
4. 项目难点及解决
4.1如何存储千亿级的坐标记录且便于检索?
4.1.1 关系型数据库的不足
4.1.2 NoSql产品选型
4.1.3 数据存储结构设计
4.1.3 高性能、高可靠性的部署
4.1.4 数据定期维护设计
4.2 如何实现标签定位的实时性与可靠性?
5. 最终方案示意
6. 补充(2015-11-13)–注意啦!
1. 系统需求
实时监控:系统能实时接收2000个超rfid标签的坐标信息, 然后在3D模型界面展示各标签的实时位置。
轨迹回放:系统可查询任一标签在最近1个月内的坐标信息, 然后在3D模型界面回放该标签在这个时间段内的运动轨迹。
2. 应用场景
监狱监管:给犯人配戴腕表或工牌,在腕表或工牌中嵌入rfid标签,实现对犯人行踪的实时监控、事先预警、事后回查等。如图:
输入图片说明
3. 数据量估算
1个超rfid标签在1秒内会产生10条80byte的坐标信息,即1个标签1秒的数据量是约0.8kb(坐标信息的结构为:标签id, x坐标,y坐标,z坐标,时间),以此推算:
1个标签 1分钟会产生记录数是 600条, 约54 KB
1个标签 1小时会产生记录数是3.6万条, 约3.2 MB
1个标签 1天会产生记录数是86.4万条, 约76.8 MB
1个标签 1个月会产生记录数是2.6亿条, 约2.3 GB
…
2000个标签 1天产生的记录数是172亿条, 约150 GB
2000个标签 1个月产生的记录数是5200亿条, 约4.4 TB
4. 项目难点及解决
4.1如何存储千亿级的坐标记录且便于检索?
如何存储海量的坐标信息,且能快速地从其中检索出指定犯人(也即标签)在某个时间范围内的坐标?
4.1.1 关系型数据库的不足
把千亿级的记录存储在关系型数据库,肯定要分库分表。如果按标签、按天分表存放,每个表存放一个标签的一天的坐标信息,那么每个表只有84.6万条记录,但有多达6万张表(=2000 X 30)。 由于轨迹回放时,是选定标签后才进行的,也即必然会根据标签检索,而从一张84.6万条记录的表中找出某个时间范围内的坐标是不成问题的。
但这方案实现起来很繁锁,因为每当新的一天来到,系统就要自动为所有标签创建新的表、并删除掉30天前创建的表;每当有新的标签加入时,也要立即为这个标签创建表。 而且,每当系统自动创建一个表后,还要为这个表创建索引,使得根据时间范围检索时能达到相应的性能要求。
以上方案,系统做了太多的DDL操作,显然不合适; 另外,由于对坐标信息的写入并没有事务方面的要求,多写或少写一条对系统几乎没有影响。 所以我们考虑用NoSql产品。
4.1.2 NoSql产品选型
NoSql产品有很多,典型的列式存储有Hbase,Key-Value型存储有Redis,文档型存储有MongoDB。
Hbase,其部署、维护较为复杂,是基于分布式文件系统的数据库。它通常不会用于仅存储某类或几类的数据,而会存储大量的各类数据,以用作分布式计算的基础。
Redis,其部署、维护都很简单,适合于对结构简单的数据提供高速缓存和持久存储。
MongoDB,其部署、维护比Redis稍复杂,其优点在于对结构不固定、不规则的数据提供存储。
由于Redis的数据结构很适合对本系统的坐标信息进行高效读写,最终选定了Redis。
4.1.3 数据存储结构设计
Redis的数据结构有以下几种:
String, 类似于java的 Map<key, String>。采用jedis的客户端代码示例:
jedis.set("key001", "value01"));
List, 类似于java的 Map<key, List<String>>。采用jedis的客户端代码示例:
jedis.lpush("key001", "定远");
jedis.lpush("key001", "致远");
List warshipList= jedis.lrange("key001", 0, -1);//从"key001" 取得所有数据项
Set, 类似于java的 Map<key, Set<String>>。采用jedis的客户端代码示例:
jedis.sadd("key001", "定远");
jedis.sadd("key001", "致远");
Set warshipSet = jedis.smembers("key001");//从"key001" 取得所有数据项
SortedSet, 与Set类似, 但提供一列属性score,用于排序。采用jedis的客户端代码示例:
//将北洋舰队的舰只名称、吨位写入"key001"
jedis.sadd("key001", 7335, "定远");
jedis.sadd("key001", 2300, "致远");
//从"key001"取得吨位在3000~8000之间的舰只名称
Set warshipSet = jedis.zrangeByScore("key001", 3000, 8000);
Hash, 类似于java的 Map<key, Map<String,String>>。采用jedis的客户端代码示例:
//将北洋舰队各舰只的基本信息写入"key001"
jedis.hset("key001", "定远", "铁甲舰,吨位7335,马力6200,1882,德国")
jedis.hset("key001", "致远", "巡洋舰,吨位2300,马力7500,1886,英国")
//从"key001" 取得所有舰只名称
List warshipList = jedis.hkeys("key001");
//从"key001" 取得"定远"的描述
String warshipDesc= jedis.hget("key001", "定远");
以上6种数据结构,第1种用来存储单个字符串,后面5种用于存储List,Set对象,但只有SortedSet提供了根据数据值的范围进行查找的功能。很显然,我们应该采用SortedSet来存储坐标信息。
如果每个key只存放一个标签的一天的坐标信息,那么就会有多达6万(=2000 X 30)个key,而Redis最多只支持 10万个key。考虑到以后可能需要支持更多标签,让每个key存放一个标签的连续三天的坐标信息,那么就只需要2万个key,于是key命名可设计为: lableId + 起始日yyyyMMdd + 终止日yyyyMMdd。
比如,可将编号为“rfid1002023”的标签的11月1日~11月3月的坐标信息写入到key: “rfid1002023-20151101-20151103″,并且把坐标时间(dd hh:mi:ss.SSS)作为score:
//标签“rfid1002023” 在11月1日17时20分21秒内,其坐标从位置(20.3, 34.0, 0.05)移动到(21.7, 34.5, 0.05)..
//则系统会以如下方式往redis写入:
jedis.sadd("rfid1002023-20151101-20151103", "01 17:20:21.008", "20.3, 34.0, 0.05");
jedis.sadd("rfid1002023-20151101-20151103", "01 17:20:21.395", "21.7, 34.5, 0.05");
//如果要查询标签“rfid1002023” 在11月1日17:15分~17:25分的记录,以便回放其在这段时间内的轨迹,则调用如下代码:
Set xyzSet = jedis.zrangeByScore("rfid1002023-20151101-20151103", "01 17:15:00", "01 17:25:00");
4.1.3 高性能、高可靠性的部署
现采用4台计算机部署两组主从模式的redis server:
A组:A-Master, A-Slave; 每台机配置1块3T的硬盘、8G以上的内存;
B组:B-Master, B-Slave; 每台机配置1块3T的硬盘、8G以上的内存;
另外采用一台计算机做监控,安装redis sentinel,以达到主从切换的目的;再将A-Master和B-Master设置为集群,就实现了读写的负载均衡。
Redis有两持久化的方式:RDB,AOF。由于RDB能更灵活地控制写入的频率,我们选择RDB方式,并将4台redis server按如下方式设置:
save 10 1 #当有1条Key的数据被改变时,10秒持久化一次
save 5 2 #当有2条Key的数据被改变时,5秒持久化一次
save 3 10 #当有10条Key的数据被改变时,3秒持久化一次
4.1.4 数据定期维护设计
Redis提供为每个key设定生命周期的功能,一旦key的存活时间超过生命周期,则这个key以及相应的数据都会都清除。所以不需要编写程序定时删除数据,让我们省了不少事。
回到本项目,我们在创建key: “rfid1002023-20151101-20151103″或者说首次写入这个key后,通过以下代码指定保存时间为1个月: jedis.expire("rfid1002023-20151101-20151103", 3600*24*31) 。 就这么简单!
4.2 如何实现标签定位的实时性与可靠性?
原始坐标信息来自基站,经路由器等网络设施传给WEB服务器,由WEB服务器对原始坐标进行相应转换,传给3D模块展现。那么,使这些坐标信息尽可能实时地、可靠地传给3D模块,是做好实时定位的关键。
前面的存储设计已经采用了Redis, 现在可以再次利用Redis的内存数据库特性。 我们再部署一组主从模式的redis server, 关闭持久化,配置主从切换。
我们应该采用Hash结构来存每个标签的最新的坐标信息,只需要设计一个key,可命名可为:“the-newest-xyz”。假如当前标签“rfid1002023”的坐标为“20.3, 34.0, 0.05”,那么以下代码就把这个坐标写入到了redis内存:
jedis.hset("the-newest-xyz", "rfid1002023", "20.3, 34.0, 0.05");
jedis.expire("the-newest-xyz",10)//将生命周期设为10s, 用于标签损坏或越防区后报警
//其它标签
jedis.hset("the-newest-xyz", "rfid1069551", "80.3, 36.0, 10.2");
这样,3D模块从这组redis server取得所有标签的坐标即可,代码如下:
List labelList = jedis.hkeys("the-newest-xyz");//取所有标签
String xyz= jedis.hget("the-newest-xyz", "rfid1069551");//取标签“rfid1069551”的坐标
5. 最终方案示意
输入图片说明
本文着重描述了系统中最为关键的坐标存储与实时读取的方案。但整个系统的实现,还需要许多基础模块的辅助,如警员信息管理、犯人信息管理、标签管理、标签绑定、楼层管理、基站管理、监区管理、坐标转换、菜单及权限管理等, 上图中的mysql就是用于存放警员信息、犯人信息、标签信息、楼层信息、权限及菜单等数据的。
6. 补充(2015-11-13)–注意啦!
感谢网友”Frank_mc”和”JavaGG”! 经他俩指出,在这里采用redis做持久存储是不可行的,因为redis启动时会加载整个数据文件到内存,这里的数据文件相对内存来说是远不够用的。
本文上述的持久化存储方案,是我预想的以为更理想的方案,也是当时(2013年)我做的备选方案。而实际项目中的存储方案是采用分目录写文件的,但由于这个方案很传统而没兴趣介绍。所以关于坐标存储这块,我没有按实际方案介绍,而是阐述我以为的理想方案。
下面,我介绍”坐标存储” 这块实际所采用的方案:
1 分目录存放文件:
- 1级目录名:年月(yyyyMM)
- 2级目录名:天(dd)
- 3级目录名:标签id
这样1级目录就1~2个,每个1级目录下有30个2级目录,每个2级目录下有2000个3级目录,每个3级目录下有24个文件。每个文件是只存放1个标签的1个小时的数据量,约3.2MB。
2 提供查询接口:设计相应算法定位到要查找的文件,载入文件内容,再进行相应的汇总、过滤。
3 定时删除文件:只保留1个月内创建的文件。
4 读写可靠性:磁盘做raid。
2015年11月14日| by: nbboy| Category: 系统设计, 缓存设计, 高性能系统
摘要 我们解决某个问题,很多时候并不在于你掌握了某个工具或某项技术,而在于你对该场景下该问题的本质理解。 这则博文,针对物联网应用中实时定位与轨迹回放的问题,阐述最为简要的解决之道。
目录[-]
1. 系统需求
2. 应用场景
3. 数据量估算
4. 项目难点及解决
4.1如何存储千亿级的坐标记录且便于检索?
4.1.1 关系型数据库的不足
4.1.2 NoSql产品选型
4.1.3 数据存储结构设计
4.1.3 高性能、高可靠性的部署
4.1.4 数据定期维护设计
4.2 如何实现标签定位的实时性与可靠性?
5. 最终方案示意
6. 补充(2015-11-13)–注意啦!
1. 系统需求
实时监控:系统能实时接收2000个超rfid标签的坐标信息, 然后在3D模型界面展示各标签的实时位置。
轨迹回放:系统可查询任一标签在最近1个月内的坐标信息, 然后在3D模型界面回放该标签在这个时间段内的运动轨迹。
2. 应用场景
监狱监管:给犯人配戴腕表或工牌,在腕表或工牌中嵌入rfid标签,实现对犯人行踪的实时监控、事先预警、事后回查等。如图:
输入图片说明
3. 数据量估算
1个超rfid标签在1秒内会产生10条80byte的坐标信息,即1个标签1秒的数据量是约0.8kb(坐标信息的结构为:标签id, x坐标,y坐标,z坐标,时间),以此推算:
1个标签 1分钟会产生记录数是 600条, 约54 KB
1个标签 1小时会产生记录数是3.6万条, 约3.2 MB
1个标签 1天会产生记录数是86.4万条, 约76.8 MB
1个标签 1个月会产生记录数是2.6亿条, 约2.3 GB
…
2000个标签 1天产生的记录数是172亿条, 约150 GB
2000个标签 1个月产生的记录数是5200亿条, 约4.4 TB
4. 项目难点及解决
4.1如何存储千亿级的坐标记录且便于检索?
如何存储海量的坐标信息,且能快速地从其中检索出指定犯人(也即标签)在某个时间范围内的坐标?
4.1.1 关系型数据库的不足
把千亿级的记录存储在关系型数据库,肯定要分库分表。如果按标签、按天分表存放,每个表存放一个标签的一天的坐标信息,那么每个表只有84.6万条记录,但有多达6万张表(=2000 X 30)。 由于轨迹回放时,是选定标签后才进行的,也即必然会根据标签检索,而从一张84.6万条记录的表中找出某个时间范围内的坐标是不成问题的。
但这方案实现起来很繁锁,因为每当新的一天来到,系统就要自动为所有标签创建新的表、并删除掉30天前创建的表;每当有新的标签加入时,也要立即为这个标签创建表。 而且,每当系统自动创建一个表后,还要为这个表创建索引,使得根据时间范围检索时能达到相应的性能要求。
以上方案,系统做了太多的DDL操作,显然不合适; 另外,由于对坐标信息的写入并没有事务方面的要求,多写或少写一条对系统几乎没有影响。 所以我们考虑用NoSql产品。
4.1.2 NoSql产品选型
NoSql产品有很多,典型的列式存储有Hbase,Key-Value型存储有Redis,文档型存储有MongoDB。
Hbase,其部署、维护较为复杂,是基于分布式文件系统的数据库。它通常不会用于仅存储某类或几类的数据,而会存储大量的各类数据,以用作分布式计算的基础。
Redis,其部署、维护都很简单,适合于对结构简单的数据提供高速缓存和持久存储。
MongoDB,其部署、维护比Redis稍复杂,其优点在于对结构不固定、不规则的数据提供存储。
由于Redis的数据结构很适合对本系统的坐标信息进行高效读写,最终选定了Redis。
4.1.3 数据存储结构设计
Redis的数据结构有以下几种:
String, 类似于java的 Map<key, String>。采用jedis的客户端代码示例:
jedis.set("key001", "value01"));
List, 类似于java的 Map<key, List<String>>。采用jedis的客户端代码示例:
jedis.lpush("key001", "定远");
jedis.lpush("key001", "致远");
List warshipList= jedis.lrange("key001", 0, -1);//从"key001" 取得所有数据项
Set, 类似于java的 Map<key, Set<String>>。采用jedis的客户端代码示例:
jedis.sadd("key001", "定远");
jedis.sadd("key001", "致远");
Set warshipSet = jedis.smembers("key001");//从"key001" 取得所有数据项
SortedSet, 与Set类似, 但提供一列属性score,用于排序。采用jedis的客户端代码示例:
//将北洋舰队的舰只名称、吨位写入"key001"
jedis.sadd("key001", 7335, "定远");
jedis.sadd("key001", 2300, "致远");
//从"key001"取得吨位在3000~8000之间的舰只名称
Set warshipSet = jedis.zrangeByScore("key001", 3000, 8000);
Hash, 类似于java的 Map<key, Map<String,String>>。采用jedis的客户端代码示例:
//将北洋舰队各舰只的基本信息写入"key001"
jedis.hset("key001", "定远", "铁甲舰,吨位7335,马力6200,1882,德国")
jedis.hset("key001", "致远", "巡洋舰,吨位2300,马力7500,1886,英国")
//从"key001" 取得所有舰只名称
List warshipList = jedis.hkeys("key001");
//从"key001" 取得"定远"的描述
String warshipDesc= jedis.hget("key001", "定远");
以上6种数据结构,第1种用来存储单个字符串,后面5种用于存储List,Set对象,但只有SortedSet提供了根据数据值的范围进行查找的功能。很显然,我们应该采用SortedSet来存储坐标信息。
如果每个key只存放一个标签的一天的坐标信息,那么就会有多达6万(=2000 X 30)个key,而Redis最多只支持 10万个key。考虑到以后可能需要支持更多标签,让每个key存放一个标签的连续三天的坐标信息,那么就只需要2万个key,于是key命名可设计为: lableId + 起始日yyyyMMdd + 终止日yyyyMMdd。
比如,可将编号为“rfid1002023”的标签的11月1日~11月3月的坐标信息写入到key: “rfid1002023-20151101-20151103″,并且把坐标时间(dd hh:mi:ss.SSS)作为score:
//标签“rfid1002023” 在11月1日17时20分21秒内,其坐标从位置(20.3, 34.0, 0.05)移动到(21.7, 34.5, 0.05)..
//则系统会以如下方式往redis写入:
jedis.sadd("rfid1002023-20151101-20151103", "01 17:20:21.008", "20.3, 34.0, 0.05");
jedis.sadd("rfid1002023-20151101-20151103", "01 17:20:21.395", "21.7, 34.5, 0.05");
//如果要查询标签“rfid1002023” 在11月1日17:15分~17:25分的记录,以便回放其在这段时间内的轨迹,则调用如下代码:
Set xyzSet = jedis.zrangeByScore("rfid1002023-20151101-20151103", "01 17:15:00", "01 17:25:00");
4.1.3 高性能、高可靠性的部署
现采用4台计算机部署两组主从模式的redis server:
A组:A-Master, A-Slave; 每台机配置1块3T的硬盘、8G以上的内存;
B组:B-Master, B-Slave; 每台机配置1块3T的硬盘、8G以上的内存;
另外采用一台计算机做监控,安装redis sentinel,以达到主从切换的目的;再将A-Master和B-Master设置为集群,就实现了读写的负载均衡。
Redis有两持久化的方式:RDB,AOF。由于RDB能更灵活地控制写入的频率,我们选择RDB方式,并将4台redis server按如下方式设置:
save 10 1 #当有1条Key的数据被改变时,10秒持久化一次
save 5 2 #当有2条Key的数据被改变时,5秒持久化一次
save 3 10 #当有10条Key的数据被改变时,3秒持久化一次
4.1.4 数据定期维护设计
Redis提供为每个key设定生命周期的功能,一旦key的存活时间超过生命周期,则这个key以及相应的数据都会都清除。所以不需要编写程序定时删除数据,让我们省了不少事。
回到本项目,我们在创建key: “rfid1002023-20151101-20151103″或者说首次写入这个key后,通过以下代码指定保存时间为1个月: jedis.expire("rfid1002023-20151101-20151103", 3600*24*31) 。 就这么简单!
4.2 如何实现标签定位的实时性与可靠性?
原始坐标信息来自基站,经路由器等网络设施传给WEB服务器,由WEB服务器对原始坐标进行相应转换,传给3D模块展现。那么,使这些坐标信息尽可能实时地、可靠地传给3D模块,是做好实时定位的关键。
前面的存储设计已经采用了Redis, 现在可以再次利用Redis的内存数据库特性。 我们再部署一组主从模式的redis server, 关闭持久化,配置主从切换。
我们应该采用Hash结构来存每个标签的最新的坐标信息,只需要设计一个key,可命名可为:“the-newest-xyz”。假如当前标签“rfid1002023”的坐标为“20.3, 34.0, 0.05”,那么以下代码就把这个坐标写入到了redis内存:
jedis.hset("the-newest-xyz", "rfid1002023", "20.3, 34.0, 0.05");
jedis.expire("the-newest-xyz",10)//将生命周期设为10s, 用于标签损坏或越防区后报警
//其它标签
jedis.hset("the-newest-xyz", "rfid1069551", "80.3, 36.0, 10.2");
这样,3D模块从这组redis server取得所有标签的坐标即可,代码如下:
List labelList = jedis.hkeys("the-newest-xyz");//取所有标签
String xyz= jedis.hget("the-newest-xyz", "rfid1069551");//取标签“rfid1069551”的坐标
5. 最终方案示意
输入图片说明
本文着重描述了系统中最为关键的坐标存储与实时读取的方案。但整个系统的实现,还需要许多基础模块的辅助,如警员信息管理、犯人信息管理、标签管理、标签绑定、楼层管理、基站管理、监区管理、坐标转换、菜单及权限管理等, 上图中的mysql就是用于存放警员信息、犯人信息、标签信息、楼层信息、权限及菜单等数据的。
6. 补充(2015-11-13)–注意啦!
感谢网友”Frank_mc”和”JavaGG”! 经他俩指出,在这里采用redis做持久存储是不可行的,因为redis启动时会加载整个数据文件到内存,这里的数据文件相对内存来说是远不够用的。
本文上述的持久化存储方案,是我预想的以为更理想的方案,也是当时(2013年)我做的备选方案。而实际项目中的存储方案是采用分目录写文件的,但由于这个方案很传统而没兴趣介绍。所以关于坐标存储这块,我没有按实际方案介绍,而是阐述我以为的理想方案。
下面,我介绍”坐标存储” 这块实际所采用的方案:
1 分目录存放文件:
- 1级目录名:年月(yyyyMM)
- 2级目录名:天(dd)
- 3级目录名:标签id
这样1级目录就1~2个,每个1级目录下有30个2级目录,每个2级目录下有2000个3级目录,每个3级目录下有24个文件。每个文件是只存放1个标签的1个小时的数据量,约3.2MB。
2 提供查询接口:设计相应算法定位到要查找的文件,载入文件内容,再进行相应的汇总、过滤。
3 定时删除文件:只保留1个月内创建的文件。
4 读写可靠性:磁盘做raid。