监控指标
-
- 1、Redis监控指标
- 2、自定义监控
-
- 2.1、前端页面
-
- [2.1.1、Bootstrap 3后台管理系统模板](#2.1.1、Bootstrap 3后台管理系统模板)
- 2.1.2、ECharts
- 2.2、WebSocket与消息推送
- 2.3、创建控制器类
- 2.4、业务逻辑
- 2.5、常用工具类
1、Redis监控指标
1.1、使用INFO命令
INFO命令会返回Redis服务器的状态信息和统计数据,使用INFO命令可以监控Redis的工作状态。可以通过以下可选参数(可选参数不区分大小写),选择查看特定分段的Redis服务器信息,比如INFO server,INFO clients等。
server:Redis服务器相关的通用信息。clients:客户端连接的相关信息。memory:内存消耗的相关信息。persistence:RDB(Redis Database)和AOF(Append-Only File)的相关信息。stats:通用统计数据。replication:主/从复制的相关信息。cpu:CPU消耗的统计数据。cluster:Redis集群的相关信息。keyspace:数据库相关的统计数据。
进入Redis客户端输入INFO命令,会返回批量字符串,是多行文本行的集合。文本行包含一个分段名称,以一个#字符开始下面显示属性,所有的属性都是以field:value的格式显示,以\r\n结尾。INFO命令的返回值如下所示。
bash
127.0.0.1:6379> INFO
# Server
redis_version:6.0.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:1c5fdf3388fb1cbf
redis_mode:standalone
os:Linux 4.14.231-173.361.amzn2.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:7.3.1
process_id:6970
run_id:19beba68248e699d5ca388d67913ba5795da43da
tcp_port:6379
uptime_in_seconds:15088
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:11766283
executable:/soft/redis-6.0.6/redis-server
config_file:/usr/local/redis/conf/redis.conf
# Clients
connected_clients:1
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
# Memory
used_memory:866648
used_memory_human:846.34K
used_memory_rss:3489792
used_memory_rss_human:3.33M
used_memory_peak:866680
used_memory_peak_human:846.37K
used_memory_peak_perc:100.00%
used_memory_overhead:820146
used_memory_startup:803016
used_memory_dataset:46502
used_memory_dataset_perc:73.08%
allocator_allocated:1119000
allocator_active:1478656
allocator_resident:4292608
total_system_memory:32891297792
total_system_memory_human:30.63G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.32
allocator_frag_bytes:359656
allocator_rss_ratio:2.90
allocator_rss_bytes:2813952
rss_overhead_ratio:0.81
rss_overhead_bytes:-802816
mem_fragmentation_ratio:4.23
mem_fragmentation_bytes:2665656
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:16986
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1622363931
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:-1
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:0
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
module_fork_in_progress:0
module_fork_last_cow_size:0
# Stats
total_connections_received:2
total_commands_processed:24
instantaneous_ops_per_sec:0
total_net_input_bytes:1488
total_net_output_bytes:55040
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:174
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
# Replication
role:master
connected_slaves:0
master_replid:32e87cdc7ba8f2a6eedfb10a1be8df27813a1a99
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
# CPU
used_cpu_sys:4.558479
used_cpu_user:7.538676
used_cpu_sys_children:0.000000
used_cpu_user_children:0.000000
# Cluster
cluster_enabled:0
# Keyspace
db0:keys=2,expires=0,avg_ttl=0
1.1.1、server分段的字段含义
bash
127.0.0.1:6379> INFO server
# Server
redis_version:6.0.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:1c5fdf3388fb1cbf
redis_mode:standalone
os:Linux 4.14.231-173.361.amzn2.x86_64 x86_64
arch_bits:64
multiplexing_api:epoll
atomicvar_api:atomic-builtin
gcc_version:7.3.1
process_id:30633
run_id:5b93f95d05073508b29296e2ca25263bb52c90e9
tcp_port:6379
uptime_in_seconds:546
uptime_in_days:0
hz:10
configured_hz:10
lru_clock:11661872
executable:/soft/redis-6.0.6/redis-server
config_file:/usr/local/redis/conf/redis.conf
server分段包含关于Redis服务器相关的信息,server分段中的主要字段的含义如下所示。
redis_version:Redis服务器版本。redis_git_sha1:Git SHA1。redis_git_dirty:Git dirty flag。redis_mode:运行模式(单机或集群)。os:Redis服务器的宿主操作系统。arch_bits:架构(32或64位)。multiplexing_api:Redis所使用的事件处理机制。atomicvar_api:Redis使用的Atomicvar API。gcc_version:用来编译这个Redis服务器的GCC编译器的版本。process_id:Redis服务器进程的进程号(PID)。run_id:用于标识Redis服务器的随机标识符(Redis的哨兵模式和集群模式会使用这个随机值)。tcp_port:TCP/IP的监听端口。uptime_in_seconds:自Redis服务器启动以来,经过的秒数。uptime_in_days:自Redis服务器启动以来,经过的天数。lru_clock:以分钟为单位进行自增的时钟,用于LRU(Least Recently Used,最近最少使用)缓存管理管理。executable:Redis服务器的可执行文件的路径。config_file:Redis服务器读取的配置文件的路径。
1.1.2、clients分段的字段含义
bash
127.0.0.1:6379> INFO clients
# Clients
connected_clients:1
client_recent_max_input_buffer:2
client_recent_max_output_buffer:0
blocked_clients:0
tracking_clients:0
clients_in_timeout_table:0
clients分段包含已连接的Redis客户端信息,clients分段中的所有字段的含义如下所示。
connected_clients:已连接客户端的数量。client_recent_max_input_buffer:当前连接的客户端当中,最大输入缓存。client_recent_max_output_buffer:当前连接的客户端当中,最大输出缓存。blocked_clients:正在等待阻塞命令(BLPOP、BRPOP、BRPOPLPUSH)的客户端的数量。tracking_clients:被跟踪的客户数量。clients_in_timeout_table:客户端超时表中的客户端数量。
1.1.3、memory分段的字段含义
bash
127.0.0.1:6379> INFO memory
# Memory
used_memory:865128
used_memory_human:844.85K
used_memory_rss:2813952
used_memory_rss_human:2.68M
used_memory_peak:866616
used_memory_peak_human:846.30K
used_memory_peak_perc:99.83%
used_memory_overhead:820114
used_memory_startup:803016
used_memory_dataset:45014
used_memory_dataset_perc:72.47%
allocator_allocated:970680
allocator_active:1265664
allocator_resident:4079616
total_system_memory:32891297792
total_system_memory_human:30.63G
used_memory_lua:37888
used_memory_lua_human:37.00K
used_memory_scripts:0
used_memory_scripts_human:0B
number_of_cached_scripts:0
maxmemory:0
maxmemory_human:0B
maxmemory_policy:noeviction
allocator_frag_ratio:1.30
allocator_frag_bytes:294984
allocator_rss_ratio:3.22
allocator_rss_bytes:2813952
rss_overhead_ratio:0.69
rss_overhead_bytes:-1265664
mem_fragmentation_ratio:3.41
mem_fragmentation_bytes:1989840
mem_not_counted_for_evict:0
mem_replication_backlog:0
mem_clients_slaves:0
mem_clients_normal:16986
mem_aof_buffer:0
mem_allocator:jemalloc-5.1.0
active_defrag_running:0
lazyfree_pending_objects:0
memory分段包含Redis服务器的内存信息,memory分段中的主要字段的含义如下所示。
used_memory:Redis分配的内存总量,以字节(Byte)为单位。used_memory_human:以用户可读的格式返回Redis分配的内存总量,以千字节(KB)为单位。used_memory_rss:从操作系统的角度,返回Redis已分配的内存总量(也被称为"常驻集大小")。这个值和top、ps等命令的输出一致,以Byte为单位。used_memory_rss_human:以用户可读的格式返回Redis已分配的内存总量,以兆字节(MB)为单位。used_memory_peak:Redis消耗的的内存峰值,以Byte为单位。used_memory_peak_human:以用户可读的格式返回Redis消耗的内存峰值,以KB为单位。used_memory_peak_perc:used_memory_peak占used_memory的百分比。used_memory_overhead:Redis服务器分配用于管理其内部数据结构的所有开销的总和,以Byte为单位。used_memory_startup:Redis在启动时消耗的初始内存量,以Byte为单位。allocator_allocated:Redis分配器分配的内存。allocator_active:Redis分配器活跃的内存。allocator_resident:Redis分配器常驻的内存。total_system_memory:Redis主机拥有的总内存量,以Byte为单位。total_system_memory_human:以用户可读的格式返回Redis主机拥有的总内存量,以吉字节(GB)为单位。used_memory_lua:Lua引擎所使用的内存大小,以Byte为单位。used_memory_lua_human:以用户可读的格式返回引擎所使用的内存大小,以GB为单位。used_memory_scripts:缓存Lua脚本使用的字节数。used_memory_scripts_human:以用户可读的格式返回缓存Lua脚本使用的字节数。maxmemory:Redis能使用的最大内存上限,0表示没有限制,以Byte为单位。maxmemory_human:以用户可读的格式返回最大内存上限。maxmemory_policy:Redis使用的的内存回收策略,可以是noeviction、allkeys-lru、volatile-lru、allkeys-random、volatile-random或volatile-ttl。mem_fragmentation_ratio:used_memory_rss和used_memory之间的比率。mem_allocator:Redis使用的内存分配器。active_defrag_running:指示活动碎片整理是否处于活动状态的标志。lazyfree_pending_objects:等待被释放的对象数(由于调用UNLINK或带有ASYNC选项的FLUSHDB和FLUSHALL)。
在理想情况下,used_memory_rss的值应该只比used_memory稍微高一点儿。当常驻集内存(rss)远大于已使用内存(used),这表示Redis服务器存在(内部或外部的)内存碎片。内存碎片的比率可以通过mem_fragmentation_ratio的值看出。当已使用内存(used)远大于常驻集内存(rss),这表示Redis的一部分内存被操作系统换出到磁盘了,在这种情况下,操作可能会产生明显的延迟。
当Redis释放内存时,分配器可能会,也可能不会,将内存返还给操作系统。如果Redis释放了内存,却没有将内存返还给操作系统,那么used_memory的值可能和操作系统显示的Redis内存占用并不一致。这可能是由于Redis正在使用和释放内存,但是释放的内存尚未归还给操作系统而导致的。查看used_memory_peak的值可以验证这种情况是否发生。
1.1.4、persistence分段的字段含义
bash
127.0.0.1:6379> INFO persistence
# Persistence
loading:0
rdb_changes_since_last_save:0
rdb_bgsave_in_progress:0
rdb_last_save_time:1622274963
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:0
rdb_current_bgsave_time_sec:-1
rdb_last_cow_size:163840
aof_enabled:0
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:-1
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_last_cow_size:0
module_fork_in_progress:0
module_fork_last_cow_size:0
persistence分段包含RDB和AOF的相关信息,persistence分段中的所有字段的含义如下所示。
loading:表示Redis是否正在加载一个转储文件的标志。rdb_changes_since_last_save:从最近一次转储至今,RDB的修改次数。rdb_bgsave_in_progress:表示Redis正在保存RDB的标志。rdb_last_save_time:最近一次成功保存RDB的时间戳,基于Epoch时间。rdb_last_bgsave_status:最近一次RDB保存操作的状态。rdb_last_bgsave_time_sec:最近一次RDB保存操作的持续时间,以秒(s)为单位。rdb_current_bgsave_time_sec:如果Redis正在执行RDB保存操作,那么这个字段表示已经消耗的时间,以s为单位。rdb_last_cow_size:最近一次RDB保存操作期间写时复制内存的大小,以Byte为单位。aof_enabled:表示AOF日志记录已激活的标志。aof_rewrite_in_progress:表示AOF重写操作正在进行的标志。aof_rewrite_scheduled:表示一旦Redis正在执行的RDB保存操作完成之后,是否就会调度执行AOF重写操作的标志。aof_last_rewrite_time_sec:最近一次AOF重写操作的持续时间,以s为单位。aof_current_rewrite_time_sec:如果Redis正在执行AOF重写操作,那么这个字段表示已经消耗的时间,以s为单位。aof_last_bgrewrite_status:最近一次AOF重写操作的状态。aof_last_write_status:对AOF的最后一次写操作的状态。aof_last_cow_size:最近一次AOF重写操作期间写时复制内存的大小,以Byte为单位。module_fork_in_progress:表示模块分叉正在进行的标志。module_fork_last_cow_size:最近一次模块fork操作期间写时复制内存的大小,以Byte为单位。
1.1.5、stats分段的字段含义
bash
127.0.0.1:6379> INFO stats
# Stats
total_connections_received:1
total_commands_processed:4
instantaneous_ops_per_sec:0
total_net_input_bytes:117
total_net_output_bytes:3594
instantaneous_input_kbps:0.00
instantaneous_output_kbps:0.00
rejected_connections:0
sync_full:0
sync_partial_ok:0
sync_partial_err:0
expired_keys:0
expired_stale_perc:0.00
expired_time_cap_reached_count:0
expire_cycle_cpu_milliseconds:0
evicted_keys:0
keyspace_hits:0
keyspace_misses:0
pubsub_channels:0
pubsub_patterns:0
latest_fork_usec:0
migrate_cached_sockets:0
slave_expires_tracked_keys:0
active_defrag_hits:0
active_defrag_misses:0
active_defrag_key_hits:0
active_defrag_key_misses:0
tracking_total_keys:0
tracking_total_items:0
tracking_total_prefixes:0
unexpected_error_replies:0
stats分段包含Redis的一般统计信息,stats分段中的主要字段的含义如下所示。
total_connections_received:Redis服务器接收的连接总数。total_commands_processed:Redis服务器处理的命令总数。instantaneous_ops_per_sec:Redis服务器每秒处理的命令数。total_net_input_bytes:Redis服务器从网络读取的总字节数。total_net_output_bytes:Redis服务器写入网络的总字节数。instantaneous_input_kbps:每秒读取数据的速率,以KB/s为单位。instantaneous_output_kbps:每秒钟发送数据的速率,以KB/s为单位。rejected_connections:Redis服务器由于maxclients限制而拒绝的连接数量。sync_full:Redis主机和从机进行完全同步的次数。sync_partial_ok:Redis服务器接受PSYNC请求的次数。sync_partial_err:Redis服务器拒绝PSYNC请求的次数。expired_keys:键过期事件的总数。expired_stale_perc:由于maxmemory限制,而被回收内存的键的总数。evicted_keys:由于maxmemory限制,而被回收内存的键的总数。keyspace_hits:在主字典中成功查找键的次数。keyspace_misses:在主字典中键查找失败的次数。pubsub_channels:客户端订阅的发布/订阅频道的总数量。pubsub_patterns:客户端订阅的发布/订阅模式的总数量。latest_fork_usec:最近一次fork操作消耗的时间,以微秒(ms)为单位。migrate_cached_sockets:迁移已缓存的套接字的数量。
1.1.6、replication分段的字段含义
bash
127.0.0.1:6379> INFO replication
# Replication
role:master
connected_slaves:0
master_replid:32e87cdc7ba8f2a6eedfb10a1be8df27813a1a99
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0
replication分段包含Redis的主/从复制信息,replication分段中的主要字段的含义如下所示。
role:如果这个Redis实例是某个Redis主机的从机,那么这个字段的值为"slave"。如果这个实例不是任何Redis主机的从机,那么这个字段的值是"master"。connected_slaves:已连接的Redis从机的数量。master_replid:Redis服务器的复制ID。master_replid2:辅助复制ID,用于故障转移后的PSYNC。master_repl_offset:Redis服务器的当前复制偏移量。second_repl_offset:接受复制ID的偏移量。repl_backlog_active:表示Redis服务器是否为部分同步开启复制备份日志(backlog)功能的标志。repl_backlog_size:备份日志的循环缓冲区的大小。repl_backlog_first_byte_offset:备份日志缓冲区中的首个字节的复制偏移量。repl_backlog_histlen:备份日志的实际数据长度。
1.1.7、cpu分段的字段含义
bash
127.0.0.1:6379> INFO cpu
# CPU
used_cpu_sys:3.919009
used_cpu_user:6.572067
used_cpu_sys_children:0.000000
used_cpu_user_children:0.000000
cpu分段包含Redis使用的CPU统计信息,cpu分段中的所有字段的含义如下所示。
used_cpu_sys:Redis服务器消耗的系统CPU性能。used_cpu_user:Redis服务器消耗的用户CPU性能。used_cpu_sys_children:后台进程消耗的系统CPU性能。used_cpu_user_children:后台进程消耗的用户CPU性能。
1.1.8、cluster分段的字段含义
bash
127.0.0.1:6379> INFO cluster
# Cluster
cluster_enabled:0
cluster分段包含Redis集群信息,cluster分段当前只包含一个字段。
cluster_enabled:表示是否启用Redis集群功能的标志。
1.1.9、keyspace分段的字段含义
bash
127.0.0.1:6379> INFO keyspace
# Keyspace
db0:keys=2,expires=0,avg_ttl=0
keyspace分段会提供关于每个数据库的主字典的统计数据。这些统计数据包括键的数量、具有过期时间的键的数量和键的平均生存时间。
对于每个Redis数据库来说,keyspace分段每行的格式如下所示。
bash
dbXXX: keys=XXX,expires=XXX
其中,第一个XXX表示数据库的编号,第二个XXX表示键的数量,第三个XXX表示具有过期时间的键的数量,第四个XXX表示键的平均生存时间。
1.2、使用redis-stat
redis-stat是基于Ruby实现的一个简单易用的Redis监控工具。redis-stat的实现原理基于Redis的INFO命令,相比Redis的MONITOR命令来说其对Redis不会产生任何性能影响。
redis-stat的使用需要依赖Ruby环境,但JRuby为我们提供了基于Java的使用方式。在GitHub官网搜索redis-stat,从redis-stat项目主页下载redis-stat-0.4.14.jar文件,如图所示。

执行启动脚本。
bash
java -jar redis-stat-0.4.14.jar 127.0.0.1:6379 3 --server=63800 > monitor.log &
启动脚本表示通过http://127.0.0.1:63800访问redis-stat,监控IP地址为127.0.0.1,端口为6379的Redis服务器,并把日志输出到monitor.log文件中。
通过Web页面直接访问http://127.0.0.1:63800,如图所示。

页面中的图形是根据Redis服务器的状态动态变化的,还是可以满足一般的监控需求的。页面指标说明如表所示。

按"F12"键打开浏览器Chrome的控制台,切换到网络选项,可以看到每5~8s刷新了一次页面,如图所示。

2、自定义监控
redis-stat提供了一种简易的方式实现对Redis的监控,但多数场景下可能需要进行定制,例如鉴权方式、统计指标、告警等。为了实现更灵活的控制,可利用类似的方式对INFO信息进行解析,以实现对Redis的统一监控。
在本节我们使用Spring Boot做一个自定义Redis监控平台,前端使用Bootstrap 3页面展示Redis的监控指标,使用WebSocket技术实时监控Redis数据库db0的keys数量,在页面中使用曲线图形动态地显示keys数量。完整的自定义监控页面如图所示。

从图中可以看出自定义监控页面中最多显示5个时间点的key数量。使用IntelliJ IDEA新建一个Spring Boot项目,命名为RedisMonitor,本实例目录为Redis\Chapter09\RedisMonitor,项目目录结构如图所示。

在监控项目RedisMonitor的pom.xml文件里引入必要的依赖包。
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.71</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.1.0</version>
</dependency>
2.1、前端页面
2.1.1、Bootstrap 3后台管理系统模板
Redis自定义监控平台的前端页面使用的是开源的模板Start Bootstrap Admin 2,它是基于Bootstrap 3开发的,是一个轻量级的后台管理系统模板。Start Bootstrap Admin 2有强大的功能组件和UI组件,基本能满足后台管理系统的需求,我们在此基础上进行二次开发,如图所示。可以从Start Bootstrap Admin2的官网下载最新的源码包,把源码包压缩后放在监控项目RedisMonitor的\src\main\resources\static目录下。

2.1.2、ECharts
Redis自定义监控平台使用的图表是ECharts。ECharts是开源的商业及数据图表库,它是一个纯JavaScript的图表库,可以流畅地运行在PC和移动设备上,兼容当前绝大部分浏览器(IE 8/9/10/11、Chrome、Firefox、Safari等),底层依赖轻量级的Canvas类库ZRender,提供直观、生动、可交互、可高度定制化的数据可视化图表。ECharts中加入了更丰富的交互功能以及更多的可视化效果,并对移动端做了深度的优化。
ECharts是由百度EFE数据可视化团队开发的,是基于JavaScript的数据图表库,在编程的灵活性和图表的丰富性方面非常强大,优点很多。
- ECharts是一款独立的Web版数据可视化工具,界面友好,提供强大的互动性操作。
- 对图形参数的修改十分简单、直观,便于初学者使用。
- 丰富的可视化图表,具有高度互动性,这得益于其完善的文档和简单的JavaScript API。相比Matplotlib的图表,ECharts更加现代和绚丽。
- 深度的交互式数据探索。ECharts提供了图例、视觉映射、数据区域缩放、Tooltip、数据筛选等开箱即用的交互组件,可以对数据进行多维度筛取、视图缩放、展示细节等交互操作。
- 移动端优化。
ECharts提供了常规的折线图、柱状图、散点图、饼图、K线图、用于统计的盒形图、用于地理数据可视化的地图、热力图、线图、用于关系数据可视化的关系图、Treemap、多维数据可视化的平行坐标,以及用于BI的漏斗图、仪表盘,并且支持图与图之间的混搭。
可以从ECharts官网下载最新的源码包,ECharts官网如图所示。

ECharts官网实例,请参考图所示页面。

首先从ECharts官网下载需要的版本。根据用户实现的功能和体积上的需求,ECharts官网提供了不同打包的下载,如果体积上没有要求,可以直接下载完整版本。本书下载的是ECharts完整版本,版本是4.1.0,文件名是echarts.min.js,如图所示。

本实例使用ECharts绘制简单的图表。新建echarts_demo01目录,在目录下新建文件夹jslib,把下载的echarts.min.js放入此文件夹中。本实例的目录结构如下所示。
echarts_demo01
└─ jslib
└─ echarts.min.js
│─ echarts_demo.html
然后在echarts_demo01目录下新建echarts_demo.html页面。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title></title>
<!-- 引入 echarts.js -->
<script src="jslib/echarts.min.js"></script>
</head>
<body>
<!-- 为ECharts准备一个具备高、宽的DOM容器 -->
<div id="main" style="width: 600px;height:400px;"></div>
<script type="text/javascript">
// 基于准备好的DOM容器,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
// 使用刚指定的配置项和数据显示图表
myChart.setOption(option);
</script>
</body>
</html>
使用浏览器打开echarts_demo.html页面,显示效果如图所示。

本实例中,我们使用ECharts绘制简单的曲线图,有以下知识点需要注意。
1.引入ECharts
ECharts是一个纯JavaScript的图表库,需要像普通的JavaScript库一样用<script>标签引入。
html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<!-- 引入ECharts文件 -->
<script src="jslib/echarts.min.js"></script>
</head>
</html>
2.绘制简单图表
在绘制简单图表前需要为ECharts准备一个具备高、宽的DOM容器。
html
<body>
<!-- 为ECharts准备一个具备高、宽的DOM容器 -->
<div id="main" style="width: 600px;height:400px;"></div>
</body>
然后就可以通过echarts.init()方法初始化一个ECharts实例并通过setOption()方法生成一个简单的曲线图。
js
<script type="text/javascript">
// 基于准备好的DOM容器,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
option = {
xAxis: {
type: 'category',
data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
},
yAxis: {
type: 'value'
},
series: [{
data: [820, 932, 901, 934, 1290, 1330, 1320],
type: 'line'
}]
};
// 使用刚指定的配置项和数据显示图表
myChart.setOption(option);
</script>
本实例中的图表的配置项参数可以参考ECharts官网文档中的配置项手册,其他图形的配置项也是如此。
本实例使用ECharts绘制了一个简单的曲线图,没有使用任何图片,显示的曲线图是通过ECharts的JavaScript函数绘制的,曲线图中的数据和互动也都是通过JavaScript函数实现的,这些JavaScript函数实现细节都封装在ECharts图表库里。
本实例中绘制的曲线图虽然在显示上是静止的,但在浏览器中是使用ECharts的JavaScript函数绘制的,支持各种动态互动功能。
2.2、WebSocket与消息推送
Redis自定义监控平台后端获得的Redis监控信息是通过WebSocket技术推送到前台页面的。WebSocket是HTML5提供的一种浏览器与服务器间进行全双工通信的网络技术,依靠这种技术可以实现客户端和服务器的长连接,实现双向实时通信,如图所示。

在客户端中,没有必要为WebSocket使用JavaScript库。实现WebSocket的Web浏览器将通过WebSocket对象公开所有必需的客户端功能(主要指支持HTML5的浏览器)。
以下API用于创建WebSocket对象。
java
var Socket = new WebSocket(url, [protocol] );
以上代码中的第一个参数url,指定连接的URL;第二个参数protocol是可选的,指定可接受的子协议。
当创建了WebSocket对象后,可以使用WebSocket对象的相关事件,如表所示。


监控页面index.html的WebSocket核心代码如下。
js
<script type="text/javascript">
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/monitor/websocket");
//console.log('websocket=' + websocket);
} else {
alert('当前浏览器 Not support WebSocket')
}
//连接发生错误的回调方法
websocket.onerror = function() {
setMessageInnerHTML("WebSocket连接发生错误");
};
//连接成功建立的回调方法
websocket.onopen = function() {
setMessageInnerHTML("WebSocket连接成功");
send();
}
//接收到消息的回调方法
websocket.onmessage = function(event) {
jsonObj = JSON.parse(event.data);
//console.log(jsonObj);
renderBoard(jsonObj);
renderChat(jsonObj);
}
function trim(x) {
return x.replace(/^\s+|\s+$/gm, '');
}
//连接关闭的回调方法
websocket.onclose = function() {
setMessageInnerHTML("WebSocket连接关闭");
}
//监听窗口关闭事件。当窗口关闭时,主动去关闭WebSocket连接,防止连接还没断开就关闭窗口,服务器会抛出异常
window.onbeforeunload = function() {
closeWebSocket();
}
//将消息显示在网页上
function setMessageInnerHTML(innerHTML) {
document.getElementById('message').innerHTML += innerHTML + '<br/>';
}
//关闭WebSocket连接
function closeWebSocket() {
websocket.close();
}
//发送消息
function send() {
var message = Math.floor(100000 * Math.random());
websocket.send(message);
}
</script>
WebSocket与Java后台交互将返回以下JSON对象格式的数据,jsonObj对象如下。
json
{
"Stats_total_commands_processed": "416",
"CPU_used_cpu_sys": "0.30",
"CPU_used_cpu_user": "0.27",
"Clients_connected_clients": "2",
"Memory_used_memory": "0.68",
"Memory_used_memory_rss": "0.64",
"time": "2019-10-07 17:10:27",
"db0_keys": "2"
}
返回的数据指标及其含义如表所示。

使用renderBoard(jsonObj)函数渲染页面上显示的网格,如图所示。

renderBoard(jsonObj)函数的核心代码如下。
java
<script type="text/javascript">
function renderBoard(jsonObj){
//console.log(jsonObj);
Memory_used_memory = jsonObj.Memory_used_memory;
Clients_connected_clients = jsonObj.Clients_connected_clients ;
Memory_used_memory_rss = jsonObj.Memory_used_memory_rss;
Stats_total_commands_processed = jsonObj.Stats_total_commands_processed;
CPU_used_cpu_sys = jsonObj.CPU_used_cpu_sys;
CPU_used_cpu_user = jsonObj.CPU_used_cpu_user;
document.getElementById('Memory_used_memory').innerHTML = Memory_used_
memory + " M";
document.getElementById('Clients_connected_clients').innerHTML = Clients_
connected_clients ;
document.getElementById('Memory_used_memory_rss').innerHTML = Memory_used_
memory_rss + " M" ;
document.getElementById('Stats_total_commands_processed').innerHTML = Stats_
total_commands_processed ;
document.getElementById('CPU_used_cpu_sys').innerHTML = CPU_used_cpu_sys ;
document.getElementById('CPU_used_cpu_user').innerHTML = CPU_used_cpu_user ;
}
</script>
使用renderChat(jsonObj)函数渲染页面上显示的动态曲线图形,如图所示。

使用ECharts绘制动态曲线图形的核心代码如下。
首先,在绘图前需要为ECharts准备一个有高和宽的DOM容器。
js
<script src="https://cdn.bootcss.com/echarts/3.7.2/echarts.min.js"></script>
<div id="main" style="width: 100%; height: 400px;"></div>
然后,通过echarts.init()方法初始化一个ECharts实例并通过setOption()方法生成一个动态的曲线图。为了实现动态曲线图的效果,曲线图最多只显示5个元素,交互数据时删除数组中第一个元素。
js
<script type="text/javascript">
var time_arr = [];
var data_arr =[];
function initChat(){
// 基于准备好的DOM容器,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));
// 指定图表的配置项和数据
option = {
title : {
text : 'keys数量'
},
tooltip : {},
xAxis: {
type: 'category',
data: time_arr
},
yAxis: {
type: 'value'
},
series: [{
data: data_arr,
type: 'line',
smooth: true
}]
};
// 使用刚指定的配置项和数据显示图表
myChart.setOption(option);
}
function renderChat(jsonObj){
keys = jsonObj.db0_keys;
time = jsonObj.time;
//console.log(keys, time);
// 使曲线图中最多显示5个元素
if (time_arr.length < 5) {
time_arr.push(time);
data_arr.push(keys);
}else{
// 删除数组中的第一个元素
time_arr.splice(0, 1);
data_arr.splice(0, 1);
time_arr[time_arr.length] = time;
data_arr[data_arr.length] = keys;
}
//console.log(time_arr.length);
initChat();
}
initChat();
</script>
2.3、创建控制器类
创建控制器类WebSocketController,调用监控类MonitorService获得Redis的性能指标。本实例使用"WebSocketController.java",内容如下。
java
package com.xinping.RedisMonitor.server;
import com.xinping.RedisMonitor.service.RedisServiceHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
@ServerEndpoint("/websocket")
@Component
public class WebSocketServer {
private static final Logger log = LoggerFactory.getLogger(WebSocketServer.class);
/**
* 当前在线连接数
*/
private static AtomicInteger onlineCount = new AtomicInteger(0);
/**
* 使用CopyOnWriteArraySet对象,concurrent包的线程安全Set,来存放每个客户端对应的
WebSocketServer对象。若要实现服务器与单一客户端通信的话,可以使用Map来存放,其中Key可以为用户标识
*/
private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new
CopyOnWriteArraySet<WebSocketServer>();
/**
* 与某个客户端的连接会话,需要通过它来给客户端发送数据
*/
private Session session;
private Thread queryThread = null;
/**
* 连接建立成功调用的方法
*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
webSocketSet.add(this); // 加入Set中
addOnlineCount(); // 在线连接数加1
log.info("有新连接加入!当前在线连接数为" + getOnlineCount());
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
webSocketSet.remove(this); // 从Set中删除
subOnlineCount(); // 在线连接数减1
log.info("有连接关闭!当前在线连接数为" + getOnlineCount());
}
public void closeQueryThread() {
if (null != queryThread) {
queryThread.interrupt();
}
}
/**
* 收到客户端消息后调用的方法
*
* @param message 客户端发送过来的消息
*/
@OnMessage
public void onMessage(String message, Session session) {
log.info("来自客户端的消息:" + message);
RedisServiceHandler handler = new RedisServiceHandler(this);
queryThread = new Thread(handler);
queryThread.start();
}
/**
* 发生错误时调用的方法
*
* @param session
* @param error
*/
@OnError
public void onError(Session session, Throwable error) {
log.error("发生错误原因:" + error.getMessage());
error.printStackTrace();
}
/**
* 实现服务器主动推送
*/
public void sendMessage(String message) throws IOException {
if (this.session.isOpen())
this.session.getBasicRemote().sendText(message);
}
public static synchronized AtomicInteger getOnlineCount() {
return onlineCount;
}
/**
* 在线连接数加1
*/
public static synchronized void addOnlineCount() {
WebSocketServer.onlineCount.getAndIncrement();
}
/**
* 在线连接数减1
*/
public static synchronized void subOnlineCount() {
WebSocketServer.onlineCount.getAndDecrement();
}
}
@ServerEndpoint("/websocket")注解是一个类层次的注解,它的功能主要是将目前的类定义成一个WebSocket服务器,注解的值将被用于监听用户连接的终端访问的URL地址,客户端可以通过这个URL地址来连接WebSocket服务器。本例创建的项目名称是monitor,部署在端口为8080的Tomcat服务器上,所以本例中WebSocket对象的访问地址是ws://localhost:8080/monitor/websocket,对应的HTML5的WebSocket核心代码如下。
java
var websocket = null;
//判断当前浏览器是否支持WebSocket
if ('WebSocket' in window) {
websocket = new WebSocket("ws://localhost:8080/monitor/websocket");
//console.log('websocket=' + websocket);
} else {
alert('当前浏览器 Not support WebSocket')
}
2.4、业务逻辑
创建监控类MonitorService,用来监控Redis状态,调用INFO命令,解析结果,并对结果进行转换处理。本实例使用了"MonitorService.java",内容如下。
java
package com.dxtd.monitor.service;
import java.util.HashMap;
import java.util.Map;
import javax.tools.Tool;
import com.dxtd.monitor.util.RedisUtil;
import com.dxtd.monitor.util.Tools;
import redis.clients.jedis.Jedis;
public class MonitorService {
//调用INFO命令
public String getInfo() {
String infoContent = null;
Jedis jedis = null;
try {
jedis = RedisUtil.getJedis();
infoContent = jedis.info();
} catch (Exception e) {
e.printStackTrace();
} finally {
if (jedis != null) {
jedis.close();
}
}
return infoContent;
}
public Double getIntValue(Map<String, String> infoMap, String key) {
if (null == infoMap)
return 0.0;
Double value = Double.valueOf(infoMap.get(key));
return value;
}
public String getStringValue(Map<String, String> infoMap, String key) {
if (null == infoMap)
return "";
return infoMap.get(key).trim();
}
public Integer getKeys(String info, String key) {
Integer keysNumValue = null;
String[] strs = info.split("\n");
if (strs != null && strs.length > 0) {
for (int i = 0; i < strs.length; i++) {
String s = strs[i].trim();
if (s.indexOf(key) > -1) {
String[] str = s.split(",");
if (null != str) {
String[] dbs = str[0].split(":");
String[] dbKeys = dbs[1].split("=");
String keyStr = dbKeys[0];
keysNumValue = Integer.valueOf(dbKeys[1]);
break;
}
}else {
keysNumValue = 0;
}
}
}
return keysNumValue;
}
//解析结果
public Map parseInfo(String content) {
String[] lines = content.split("\n");
Map infoMap = new HashMap();
String part = null;
for (String line : lines) {
if (line.isEmpty()) {
continue;
}
if (line.startsWith("#")) {
part = line.replace("#", "").trim();
continue;
}
int index = line.indexOf(':');
if (index >= 0) {
infoMap.put(part + "_" + line.substring(0, index), line.substring
(index + 1));
}
}
return infoMap;
}
//数据转换处理
public void transData(String infoContent) {
Map<String, String> infoMap = parseInfo(infoContent);
// 内核空间占用CPU的百分比
String ucs = getStringValue(infoMap, "CPU_used_cpu_sys");
// 用户空间占用CPU的百分比
String ucu = getStringValue(infoMap, "CPU_used_cpu_user");
// 阻塞客户端的数量
String cbc = getStringValue(infoMap, "Clients_blocked_clients");
// 连接客户端的数量
String ccc = getStringValue(infoMap, "Clients_connected_clients");
// 使用总内存
String mum = getStringValue(infoMap, "Memory_used_memory");
// 使用物理内存
String mur = getStringValue(infoMap, "Memory_used_memory_rss");
// 运行以来执行过的命令的总数量
String cmd = getStringValue(infoMap, "Stats_total_commands_processed");
// 每秒过期key数量
String exp = getStringValue(infoMap, "Stats_expired_keys");
// 每秒淘汰key数量
String evt = getStringValue(infoMap, "Stats_evicted_keys");
// 每秒命中数量
String hit = getStringValue(infoMap, "Stats_keyspace_hits");
// 每秒丢失数量
String mis = getStringValue(infoMap, "Stats_keyspace_misses");
Integer db0keysNum = getKeys(infoContent, "db0_keys");
long thisTs = System.currentTimeMillis();
System.out.println("ucs=" + ucs + ",ucu=" + ucu + ",cbc=" + cbc);
System.out.println("ccc=" + ccc + ",mum=" + mum + ",mur=" + mur);
System.out.println("cmd=" + cmd + ",exp=" + exp);
System.out.println("evt=" + evt + ",hit=" + hit + ",mis=" + mis);
System.out.println("db0keysNum=" + db0keysNum);
}
public Map<String, Object> getRedisInfo(String infoContent) {
Map<String, Object> map = new HashMap<String, Object>();
Map<String, String> infoMap = parseInfo(infoContent);
// 使用总内存
String mum = getStringValue(infoMap, "Memory_used_memory");
// 连接客户端的数量
String ccc = getStringValue(infoMap, "Clients_connected_clients");
// 使用物理内存
String mur = getStringValue(infoMap, "Memory_used_memory_rss");
// 运行以来执行过的命令的总数量
String cmd = getStringValue(infoMap, "Stats_total_commands_processed");
// 内核空间占用CPU的百分比
String ucs = getStringValue(infoMap, "CPU_used_cpu_sys");
// 用户空间占用CPU的百分比
String ucu = getStringValue(infoMap, "CPU_used_cpu_user");
Integer db0keysNum = getKeys(infoContent, "db0:keys");
map.put("Memory_used_memory", Tools.transByteToMBSize(Integer.valueOf(mum )) );
map.put("Clients_connected_clients", ccc);
map.put("Memory_used_memory_rss", Tools.transByteToMBSize(Integer.valueOf
(mur )) );
map.put("Stats_total_commands_processed", cmd);
map.put("CPU_used_cpu_sys", ucs);
map.put("CPU_used_cpu_user", ucu);
map.put("db0_keys", db0keysNum+"");
map.put("time", Tools.getCurrntTime());
return map;
}
}
创建RedisServiceHandler类实现一个线程,在这个线程的run()方法里,生成MonitorService对象来监控Redis的状态,并对INFO命令的返回值进行处理,然后把返回结果返还给WebSocket的前端页面。本实例使用"RedisServiceHandler.java",内容如下。
java
package com.dxtd.monitor.service;
import java.io.IOException;
import java.util.Map;
import com.alibaba.fastjson.JSON;
import com.dxtd.monitor.action.WebSocketController;
public class RedisServiceHandler implements Runnable {
private WebSocketController webSocket;
public RedisServiceHandler(WebSocketController webSocket){
this.webSocket = webSocket;
}
ublic void run() {
while(true){
try {
MonitorService monitor = new MonitorService();
String info = monitor.getInfo();
Map<String,Object> redisInfo = monitor.getRedisInfo(info);
//群发消息
webSocket.sendMessage(JSON.toJSONString(redisInfo) );
//每5s发送一次消息,以便页面更新数据
Thread.sleep(1000 * 5 );
} catch (IOException e) {
e.printStackTrace();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
2.5、常用工具类
RedisUtil类是连接Redis的工具类,封装了Jedis连接池的业务逻辑。本实例使用"RedisUtil.java",内容如下。
java
package com.dxtd.monitor.util;
import redis.clients.jedis.Client;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
public class RedisUtil {
private static JedisPool jedisPool;
/**
* 建立Redis的连接池,如果需要优化可以把连接池的配置参数单独抽取出来
*
*/
private static void createJedisPool() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//指定连接池中最大空闲连接数
jedisPoolConfig.setMaxIdle(10);
//连接池中创建的最大连接数
jedisPoolConfig.setMaxTotal(100);
//设置创建连接的超时时间
jedisPoolConfig.setMaxWaitMillis(2000);
//表示连接池在创建连接的时候会先测试一下连接是否可用,这样可以保证连接池中的连接都可用
jedisPoolConfig.setTestOnBorrow(true);
// 创建连接池
jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379);
}
/**
* 在多线程环境同步初始化
*/
private static synchronized void poolInit() {
if (jedisPool == null)
createJedisPool();
}
/**
* 获取一个Jedis对象
*
* @return
*/
static {
if (jedisPool == null)
poolInit();
}
/**
* 归还一个连接
*
* @param jedis
*/
public static void returnRes(Jedis jedis) {
jedis.close();
}
public static Jedis getJedis() {
if (jedisPool == null)
poolInit();
Jedis jedis = jedisPool.getResource();
return jedis;
}
/**
* 获取Redis服务器信息
*
* @param String
*/
public String getRedisInfo() {
if (jedisPool == null)
poolInit();
Jedis jedis = null;
try {
jedis = jedisPool.getResource();
Client client = jedis.getClient();
client.info();
String info = client.getBulkReply();
return info;
} finally {
// 返还到连接池
if(null != jedis)
jedis.close();
}
}
}
常用工具类Tools,用于获得格式化后的时间和转化单位。本实例使用"Tools.java",内容如下。
java
package com.dxtd.monitor.util;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Tools {
//获得格式化后的时间
public static String getCurrntTime() {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date now = new Date();
return format.format(now);
}
// 转化单位,从Byte到MB
public static String transByteToMBSize(int size) {
int MB = 1024 * 1024;// 定义1024MB的计算存储单位
DecimalFormat df = new DecimalFormat("0.00");// 格式化小数
String resultSize = "";
resultSize = df.format(size / (float) MB);
return resultSize;
}
}