lazytimes

lazytimes 查看完整档案

深圳编辑湖南化工职业技术学院  |  通用软件 编辑中付  |  java后端 编辑 lazytime.site/ 编辑
编辑

赐他一块白石,石上写着新名
日常摸鱼,有空才写博客
掘金地址:https://juejin.im/user/299912...
博客地址:http://lazytime.site
微信公众号:懒时小窝(刚起步)

个人动态

lazytimes 发布了文章 · 11月29日

Redis学习 - 复制以及三种部署模式

Redis学习 - 复制以及三种部署模式

什么是复制

单机的redis通常情况是无法满足项目需求的,一般都建议使用集群部署的方式进行数据的多机备份和部署,这样既可以保证数据安全,同时在redis宕机的时候,复制也可以对于数据进行快速的修复。

<!-- more -->

采取的方式

  1. 单机部署(忽略)
  2. 主从链
  3. 一主多从
  4. 哨兵模式
  5. 集群模式

复制的前提

  1. 需要保证redis.conf里面的配置是正确的,比如:
dir ./
dbfilename "dump.rdb"
  1. 需要保证指定的路径对于redis来说是可写的,意味着如果当前目录没有写权限同样会失败

从服务器连接主服务器的几种方式

  1. 在从服务器的配置文件里面配置连接那个主服务器:

连接的具体配置如下:

在5.0版本中使用了replicaof代替了slaveofgithub.com/antirez/red…),slaveof还可以继续使用,不过建议使用replicaof

下面是个人的配置

# replicaof <masterip> <masterport> 
replicaof 127.0.0.1 16379
警告:此小节只说明了这一个配置的更改,进行主从配置的时候还有其他几个参数需要更改,这里只作为部分内容参考
  1. 在启动的适合,在redis从服务器的redis-cli当中敲击如下的命令:
127.0.0.1:16380> slaveof 127.0.0.1 16379
OK Already connected to specified master

这样就可以在从服务器动态的指定要连接哪个主服务器了,但是这种配置是当前运行时有效,下次再次进入的时候,会根据配置文件进行配置或者按照默认的规则当前实例就是master3.

  1. 在从服务器执行slaveof no one,当前实例脱离控制自动成为master

redis 复制启动的过程==(重点)==

主服务器操作从服务器操作
1. (等待命令)1. 连接(重新连接)主服务器,发送sync命令
2. 开始执行bgsave,使用缓冲区记录bgsave之后执行所有写命令2. 根据配置选项是使用现有的数据(存在)处理客户端请求,还是向请求的客户端返回错误信息
3. bgsave执行完毕,向从服务器发送快照文件,同时异步执行缓冲区记录的写命令3. 丢弃所有的旧数据,载入主服务器的快照文件
4. 快照文件发送完毕,开始向着从服务器发送存储在缓冲区的写命令4. 完成对于快照的解释操作,恢复日常的请求操作
5. 缓冲区写命令发送完成,同时现在每执行一个写命令就像从服务器发送相同写命令5. 执行主服务器发来的所有存储在缓冲区的写命令,并且从现在开始接受主服务器的每一个命令
建议:由于bgsave需要开启进行子线程的创建写入缓冲区的创建,所以最好在系统中预留30% - 45% 内存用于redis的bgsave操作

特别注意:当从服务器连接主服务器的那一刻,执行到第三步会清空当前redis里面的所有数据。

配置方式和命令方式的区别:

redis.conf 配置slaveof 的方式:不会马上进行主服务器同步,而是先载入当前本地存在的rdb或者aof到redis中进行数据恢复,然后才开始同步复制

命令slaveof方式:会立即连接主服务器进行同步操作

关于redis的主主复制:

如果我们尝试让两台服务器互相slaveof 那么会出现上面情况呢?

从上面的复制过程可以看到,当一个服务器slaveof另一个服务器,产生的结果只会是两边相互覆盖,也就是从服务器会去同步主服务器的数据,如果此时按照主主的配置,两边互相同步对方的数据,这样产生的数据可能会不一致,或者数据干脆就是不完整的。不仅如此,这种操作还会大量占用资源区让两台服务器互相知道对方

当一台服务器连接另一台服务器的时候会发生什么?

当有新服务器连接的时候主服务器操作
步骤3还没有执行所有从服务器都会收到相同的快照文件和相同缓冲区写命令
步骤3正在执行或者已经执行完成了之前同步的五个操作之后,会跟新服务器重新执行一次新的五个步骤

系统故障处理

复制和持久化虽然已经基本可以保证系统的数据安全,但是总有意外的情况,比如突然断电断网,系统磁盘故障,服务器宕机等一系列情况,那么会出现各种莫名奇妙的问题,下面针对这些情况说明一下解决方式:

验证快照文件以及aof文件

在redis的bin目录下面,存在如下的两个sh

-rwxr-xr-x 1 root root 9722168 Nov 15 20:53 redis-check-aof
-rwxr-xr-x 1 root root 9722168 Nov 15 20:53 redis-check-rdb

他们的命令作用和内容如下:

[xd@iZwz99gyct1a1rh6iblyucZ bin]$ ./redis-check-aof 
Usage: ./redis-check-aof [--fix] <file.aof>
[xd@iZwz99gyct1a1rh6iblyucZ bin]$ ./redis-check-rdb 
Usage: ./redis-check-rdb <rdb-file-name>

redis-check-aof:如果加入--fix选项,那么命令会尝试修复aof文件,会将内容里面出现错误的命令以及下面的所有命令清空,一般情况下回清空尾部的一些未完成命令。

redis-check-rdb:遗憾的是目前这种修复收效甚微。建议在修复rdb的时候,用SHA1和SHA256验证文件是否完整。

校验和与散列值:

redis2.6 之后加入了校验和与散列值进行验证。

快照文件增加CRC64校验和

什么是crc循环冗余校验

https://zh.wikipedia.org/wiki...

更换故障主服务器:

  1. 假设A故障,存在BC两台机器,B为从服务,C为将要替换的主服务器
  2. 向机器B发送save命令,同时创建一个新的快照文件,同步完成之后,发送给C
  3. 机器C上面启动redis,让C成为B的主服务器

Redis sentienel 哨兵

可以监视指定主服务器以及属下的从服务器

也就是我们常用的哨兵模式

但是随着时代进步,目前使用redis基本还是以cluster模式为主

redis主从复制模式(redis6.0版本):

前提说明:

有条件的可以弄三台虚拟机查看效果,这样模拟出来的效果算是比较真实的。

三台从服务器以及一台主服务器的配置

个人的办法是copy一个公用的配置,然后进行修改(这里只列举区别以及改动较多的地方,其他地方根据需要配置):

第一台机器的配置:

pidfile /var/run/redis_16379.pid
port 16379
dbfilename dump16379.rdb
appendfilename "appendonly16379.aof"
logfile "log16379"

第二台机器的配置:

pidfile /var/run/redis_16380.pid
port 16380
dbfilename dump16380.rdb
appendfilename "appendonly16380.aof"
logfile "log16380"

第三台机器的配置:

pidfile /var/run/redis_16381.pid
port 16381
dbfilename dump16381.rdb
appendfilename "appendonly16381.aof"
logfile "log16381"

这时候要配置一台主服务器

pidfile /var/run/redis_10000.pid
port 10000
dbfilename dump10000.rdb
appendfilename "appendonly10000.aof"
logfile "log10000"

启动redis一主多从:

配置很简单,可以用手动进行主从复制,也可以使用redis.conf提前配置,具体区别上文已经进行过介绍,这里不再赘述。

从服务器可以通过命令:slaveof 127.0.0.1 10000 实现主从复制拷贝

可以通过命令info replication 查看主从配置的信息。

主服务器启动日志:

127.0.0.1:10000> info replication
# Replication
role:master
connected_slaves:0
master_replid:e2a92d8c59fbdde3b162da12f4d74ff28bab4fbb
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
127.0.0.1:10000> info replication
# Replication
role:master
connected_slaves:3
slave0:ip=127.0.0.1,port=16381,state=online,offset=14,lag=1
slave1:ip=127.0.0.1,port=16380,state=online,offset=14,lag=1
slave2:ip=127.0.0.1,port=16379,state=online,offset=14,lag=1
master_replid:029e455ee6f8fdc0e255b6d5c4f63136d933fb24
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:14
second_repl_offset:-1
repl_backlog_active:1
repl_backlog_size:1048576
repl_backlog_first_byte_offset:1
repl_backlog_histlen:14

可以看到进行主从配置之后,当前的目录下面多出了对应备份文件

当进行主从配置之后,从服务就无法进行写入了,主服务器才可以写入:

127.0.0.1:16379> set key 1
(error) READONLY You can't write against a read only replica.

测试一主多从复制:

主服务器敲入如下命令:

127.0.0.1:10000> hset key1 name1 value1
(integer) 1
127.0.0.1:10000> keys *
1) "key1"

从服务器:

127.0.0.1:16379> hget key1 name1
"value1"
127.0.0.1:16380> hget key1 name1
"value1"
127.0.0.1:16381> hget key1 name1
"value1"

主从链

配置方式:

和主从配置一样,只不过主节点换为从节点。

注意:主从链的配置依然只能是master节点可以写数据,同时中间的节点也是slave

扩展

如何检测磁盘是否写入数据?

  1. 主从服务器通过一个虚标值(unique dummy value)来验证从服务器是否真的把数据写入到自己的磁盘。
  2. 通过命令:info检查结果当中的 aof_appending_bio_fsync的值是否为0:
\# 5.0 版本之后改为如下形式验证

repl_backlog_active:0

redis主从哨兵模式(Redis sentienel)(redis6.0版本)

哨兵模式有什么作用:

Redis的哨兵模式就是对redis系统进行实时的监控,其主要功能有下面两点

1.监测主数据库和从数据库是否正常运行。

2.当我们的主数据库出现故障的时候,可以自动将从数据库转换为主数据库,实现自动的切换

为什么要使用哨兵模式:

  1. 主从复制在主节点宕机的情况下,需要人工干预恢复redis,无法实现高可用。
  2. 主节点宕机的情况下需要备份数据到新的从节点,然后其他节点将主节点设置为新的redis,需要一次全量复制同步数据的过程

哨兵模式原理

主节点故障的时候,由redis sentinel自动完成故障发现和转移

如何部署哨兵模式:

  1. 首先按照上一节配置,已经设置了一个主节点三个从节点的配置
下面的配置如下:

主节点:10000

从节点1:16379

从节点2:16380

从节点3:16381

[xd@iZwz99gyct1a1rh6iblyucZ ~]$ ps -ef | grep redis
xd        2964  2910  0 18:02 pts/0    00:00:00 grep --color=auto redis
root     26412     1  0 Nov23 ?        00:06:07 ./redis-server 127.0.0.1:10000
root     26421     1  0 Nov23 ?        00:05:37 ./redis-server 127.0.0.1:16379
root     26428     1  0 Nov23 ?        00:05:37 ./redis-server 127.0.0.1:16380
root     26435     1  0 Nov23 ?        00:05:37 ./redis-server 127.0.0.1:16381
  1. sentinel.conf 配置文件在安装redis的源码包里面有,所以如果误删了可以下回来然后把文件弄到手,其实可以配置一个常用的或者通用的配置放到自己的本地有需要直接替换
  2. 配置5个sentienl.conf文件(建议奇数个哨兵,方便宕机选举产生新的节点)
[xd@iZwz99gyct1a1rh6iblyucZ bin]$ sudo cp sentinel.conf sentinel_26379.conf
[xd@iZwz99gyct1a1rh6iblyucZ bin]$ sudo cp sentinel.conf sentinel_26380.conf
[xd@iZwz99gyct1a1rh6iblyucZ bin]$ sudo cp sentinel.conf sentinel_26381.conf
[xd@iZwz99gyct1a1rh6iblyucZ bin]$ sudo cp sentinel.conf sentinel_10000.conf
  1. 四个配置文件的改动依次如下:

所有的sentinel.conf 配置如下:

# 指定哨兵端口
port 20000
# 监听主节点10000
sentinel monitor mymaster 127.0.0.1 10000 2
# 连接主节点时的密码,如果redis配置了密码需要填写
sentinel auth-pass mymaster 12345678
# 故障转移时最多可以有2从节点同时对新主节点进行数据同步
sentinel config-epoch mymaster 2
# 故障转移超时时间180s,
sentinel failover-timeout mymasterA 180000 
# sentinel节点定期向主节点ping命令,当超过了300S时间后没有回复,可能就认定为此主节点出现故障了……
sentinel down-after-milliseconds mymasterA 300000
# 故障转移后,1代表每个从节点按顺序排队一个一个复制主节点数据,如果为3,指3个从节点同时并发复制主节点数据,不会影响阻塞,但存在网络和IO开销
sentinel parallel-syncs mymasterA 1
# 设置后台启动
daemonize yes
# 进程的pid文件,保险起见设置不一样的,特别是设置后台启动的时候
pidfile /var/run/redis-sentinel.pid
扩展:如何判定转移失败:

a - 如果转移超时失败,下次转移时时间为之前的2倍;

b - 从节点变主节点时,从节点执行slaveof no one命令一直失败的话,当时间超过180S时,则故障转移失败

c - 从节点复制新主节点时间超过180S转移失败

下面为配好五个之后的配置:

-rw-r--r-- 1 root root   10772 Nov 28 21:00 sentienl_26382.conf
-rw-r--r-- 1 root root   10767 Nov 28 20:43 sentinel_10000.conf
-rw-r--r-- 1 root root   10772 Nov 28 21:03 sentinel_26379.conf
-rw-r--r-- 1 root root   10766 Nov 28 20:46 sentinel_26380.conf
-rw-r--r-- 1 root root   10772 Nov 28 20:59 sentinel_26381.conf
-rw-r--r-- 1 root root   10772 Nov 28 21:03 sentinel_26382.conf
-rw-r--r-- 1 root root   10744 Nov 28 18:06 sentinel.conf
  1. 上一节已经启动过,这里不再介绍
  2. 启动sentinel服务

启动五个哨兵:

./redis-sentinel ./sentinel_10000.conf 
./redis-sentinel ./sentinel_26379.conf 
./redis-sentinel ./sentinel_263780.conf 
./redis-sentinel ./sentinel_263781.conf 
./redis-sentinel ./sentinel_263782.conf 

使用ps命令查看所有的服务:

root      3267     1  0 21:14 ?        00:00:01 ./redis-sentinel *:20000 [sentinel]
root      3280     1  0 21:15 ?        00:00:01 ./redis-sentinel *:26379 [sentinel]
root      3296     1  0 21:20 ?        00:00:00 ./redis-sentinel *:26380 [sentinel]
root      3303     1  0 21:21 ?        00:00:00 ./redis-sentinel *:26381 [sentinel]
root      3316  3254  0 21:28 pts/0    00:00:00 grep --color=auto redis
root     26412     1  0 Nov23 ?        00:06:17 ./redis-server 127.0.0.1:10000
root     26421     1  0 Nov23 ?        00:05:47 ./redis-server 127.0.0.1:16379
root     26428     1  0 Nov23 ?        00:05:47 ./redis-server 127.0.0.1:16380
root     26435     1  0 Nov23 ?        00:05:47 ./redis-server 127.0.0.1:16381
  1. 验证一下哨兵是否管用

10000是主节点,他的info信息如下:

# Keyspace
db0:keys=1,expires=0,avg_ttl=0
127.0.0.1:10000> info replication
# Replication
role:master
connected_slaves:3

使用kill -9 master节点进程端口号之后,我们已经干掉了额主进程,验证一下从节点是否启动

进入到6379端口的redis-cli当中,可以看到从节点6379的实例被选举为新的的节点

127.0.0.1:16379> info replication
# Replication
role:master
connected_slaves:2
slave0:ip=127.0.0.1,port=16380,state=online,offset=857706,lag=1
slave1:ip=127.0.0.1,port=16381,state=online,offset=858242,lag=1

挂掉的主节点恢复之后,能不能进行恢复为主节点?

尝试重启挂掉的master之后,可以发现他变成了从节点

127.0.0.1:10000> info replication
# Replication
role:slave
master_host:127.0.0.1
master_port:16379
master_link_status:up
master_last_io_seconds_ago:2

注意:生产环境建议让redis Sentinel部署到不同的物理机上

如果不喜欢上面的启动哨兵模式,也可以使用下面的命令开启:

[root@dev-server-1 sentinel]# redis-server sentinel1.conf --sentinel
[root@dev-server-1 sentinel]# redis-server sentinel2.conf --sentinel
[root@dev-server-1 sentinel]# redis-server sentinel3.conf --sentinel

哨兵模式部署建议

a,sentinel节点应部署在多台物理机(线上环境)

b,至少三个且奇数个sentinel节点

c,通过以上我们知道,3个sentinel可同时监控一个主节点或多个主节点

监听N个主节点较多时,如果sentinel出现异常,会对多个主节点有影响,同时还会造成sentinel节点产生过多的网络连接,

一般线上建议还是, 3个sentinel监听一个主节点

也可以按照下面的方式在启动哨兵的时候启动:

哨兵模式的优缺点:

优点:

  1. 哨兵模式基于主从复制模式,所以主从复制模式有的优点,哨兵模式也有
  2. 哨兵模式下,master挂掉可以自动进行切换,系统可用性更高

缺点:

  1. 同样也继承了主从模式难以在线扩容的缺点,Redis的容量受限于单机配置
  2. 需要额外的资源来启动sentinel进程,实现相对复杂一点,同时slave节点作为备份节点不提供服务

redis集群模式(redis6.0版本)

随着应用的扩展,虽然主从模式和哨兵模式的加入解决了高可用的问题,但是现代的应用基本都是要求可以动态扩展了,为了支持动态扩展,redis在后续的版本当中加入了哨兵的模式

集群模式主要解决的问题是:

Cluster模式实现了Redis的分布式存储,即每台节点存储不同的内容,来解决在线扩容的问题

redis结构设计:

使用的是无中心的结构,每一个节点和节点之间相互连接

  1. redis 使用彼此互联的(ping-pong)的方式,进行互相关联,内部使用二进制协议优化速度
  2. 客户端与redis节点直连,不需要中间代理层.客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可
  3. 节点的fail是通过集群中超过半数的节点检测失效时才生效

redis集群的工作机制

  1. 在Redis的每个节点上,都有一个插槽(slot),取值范围为0-16383,redis会根据接节点的数量分配槽的位置来进行判定发送给哪一个cluster节点
  2. 当我们存取key的时候,Redis会根据CRC16的算法得出一个结果,然后把结果对16384求余数,这样每个key都会对应一个编号在0-16383之间的哈希槽,通过这个值,去找到对应的插槽所对应的节点,然后直接自动跳转到这个对应的节点上进行存取操作
  3. 为了保证高可用,Cluster模式也引入主从复制模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点
  4. 当其它主节点ping一个主节点A时,如果半数以上的主节点与A通信超时,那么认为主节点A宕机了。如果主节点A和它的从节点都宕机了,那么该集群就无法再提供服务了

配置集群(重点):

为了不产生干扰,先把上一节所有的redis进程干掉,包括哨兵的配置

使用kil -9 进程端口号直接抹掉整个应用

配置如下:

  1. 集群至少需要三主三从,同时需要奇数的节点配置。
  2. 我们可以将之前的主从配置的一主三从增加两个主节点,目前的配置如下:
-rw-r--r-- 1 root root   84993 Nov 28 21:41 redis10000.conf
-rw-r--r-- 1 root root   84936 Nov 28 21:35 redis16379.conf
-rw-r--r-- 1 root root   84962 Nov 28 21:35 redis16380.conf
-rw-r--r-- 1 root root   84962 Nov 28 21:35 redis16381.conf

# 增加两个主要节点
-rw-r--r-- 1 root root   84962 Nov 28 21:35 redis16382.conf
-rw-r--r-- 1 root root   84962 Nov 28 21:35 redis16383.conf

主节点的配置主要如下:

port 7100 # 本示例6个节点端口分别为7100,7200,7300,7400,7500,7600 
daemonize yes # r后台运行 
pidfile /var/run/redis_7100.pid # pidfile文件对应7100,7200,7300,7400,7500,7600 
cluster-enabled yes # 开启集群模式 
masterauth passw0rd # 如果设置了密码,需要指定master密码
cluster-config-file nodes_7100.conf # 集群的配置文件,同样对应7100,7200等六个节点
cluster-node-timeout 15000 # 请求超时 默认15秒,可自行设置 

启动如下:

[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17000_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17100_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17200_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17300_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17400_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-server ./cluster/redis17500_cluster.conf
[root@iZwz99gyct1a1rh6iblyucZ bin]# ps -ef | grep redis
root      4761     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17000 [cluster]
root      4767     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17100 [cluster]
root      4773     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17200 [cluster]
root      4779     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17300 [cluster]
root      4785     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17400 [cluster]
root      4791     1  0 15:55 ?        00:00:00 ./redis-server 127.0.0.1:17500 [cluster]
root      4797  4669  0 15:55 pts/0    00:00:00 grep --color=auto redis

启动了上面六个节点之后,使用下面的命令并且敲入yes让他们变为集群:

[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-cli --cluster create 127.0.0.1:17000 127.0.0.1:17100 127.0.0.1:17200 127.0.0.1:17300 127.0.0.1:17400 127.0.0.1:17500 --cluster-replicas 1

>>> Performing hash slots allocation on 6 nodes...
Master[0] -> Slots 0 - 5460
Master[1] -> Slots 5461 - 10922
Master[2] -> Slots 10923 - 16383
Adding replica 127.0.0.1:17400 to 127.0.0.1:17000
Adding replica 127.0.0.1:17500 to 127.0.0.1:17100
Adding replica 127.0.0.1:17300 to 127.0.0.1:17200
>>> Trying to optimize slaves allocation for anti-affinity
[WARNING] Some slaves are in the same host as their master
M: 1179bb5f47e7f8221ba7917b5852f8064778e0db 127.0.0.1:17000
   slots:[0-5460] (5461 slots) master
M: 153afa1b9b14194de441fffa791f8d9001badc66 127.0.0.1:17100
   slots:[5461-10922] (5462 slots) master
M: 4029aeeb6b80e843279738d6d35eee7a1adcd2ff 127.0.0.1:17200
   slots:[10923-16383] (5461 slots) master
S: 3ceb11fe492f98432f124fd1dcb7b2bb1e769a96 127.0.0.1:17300
   replicates 1179bb5f47e7f8221ba7917b5852f8064778e0db
S: 66eaea82ccf69ef96dbc16aac39fd6f6ed3d0691 127.0.0.1:17400
   replicates 153afa1b9b14194de441fffa791f8d9001badc66
S: c34aeb59c8bedc11b4aeb720b70b0019e7389093 127.0.0.1:17500
   replicates 4029aeeb6b80e843279738d6d35eee7a1adcd2ff

验证集群:

  1. 输入redis-cli进入任意的一个主节点,注意是主节点,从节点不能做写入操作

Redirected to slot [9189] located at 127.0.0.1:17100根据Hash的算法,算出连接那个节点槽,然后提示slot[9189] 落到了17100上面,所以集群会自动跳转进行Key的加入

[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-cli -p 17000
127.0.0.1:17000> set key1 1
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-cli -p 17000
127.0.0.1:17000> set key1 1
(error) MOVED 9189 127.0.0.1:17100
[root@iZwz99gyct1a1rh6iblyucZ bin]# ./redis-cli -p 17000 -c
127.0.0.1:17000> set key1 ke
-> Redirected to slot [9189] located at 127.0.0.1:17100
OK
小贴士:集群之后不能使用传统的连接方式,因为每一个key都要经过一次hash的操作找到对应的槽 -》节点之后才能做后续的操作

使用如下命令进入后正常

./redis-cli -p 17000 -c

-c 代表以集群的方式连接

  1. 可以使用如下命令验证集群的信息:
127.0.0.1:17000> cluster nodes
66eaea82ccf69ef96dbc16aac39fd6f6ed3d0691 127.0.0.1:17400@27400 slave 153afa1b9b14194de441fffa791f8d9001badc66 0 1606639411000 2 connected
4029aeeb6b80e843279738d6d35eee7a1adcd2ff 127.0.0.1:17200@27200 master - 0 1606639411000 3 connected 10923-16383
3ceb11fe492f98432f124fd1dcb7b2bb1e769a96 127.0.0.1:17300@27300 slave 1179bb5f47e7f8221ba7917b5852f8064778e0db 0 1606639410000 1 connected
1179bb5f47e7f8221ba7917b5852f8064778e0db 127.0.0.1:17000@27000 myself,master - 0 1606639410000 1 connected 0-5460
153afa1b9b14194de441fffa791f8d9001badc66 127.0.0.1:17100@27100 master - 0 1606639412002 2 connected 5461-10922
c34aeb59c8bedc11b4aeb720b70b0019e7389093 127.0.0.1:17500@27500 slave 4029aeeb6b80e843279738d6d35eee7a1adcd2ff 0 1606639413005 3 connected

  1. 接下来我们验证一下当一个主节点挂掉会发生什么情况:

还是和主从复制的验证一样,直接Kill 进程:

kill掉 17000 之后,我们可以发现 17300 被升级为主节点

127.0.0.1:17300> info replication
# Replication
role:master
connected_slaves:0

此时的节点情况如下:

127.0.0.1:17100> cluster nodes
4029aeeb6b80e843279738d6d35eee7a1adcd2ff 127.0.0.1:17200@27200 master - 0 1606640582000 3 connected 10923-16383
153afa1b9b14194de441fffa791f8d9001badc66 127.0.0.1:17100@27100 myself,master - 0 1606640581000 2 connected 5461-10922
66eaea82ccf69ef96dbc16aac39fd6f6ed3d0691 127.0.0.1:17400@27400 slave 153afa1b9b14194de441fffa791f8d9001badc66 0 1606640581000 2 connected
c34aeb59c8bedc11b4aeb720b70b0019e7389093 127.0.0.1:17500@27500 slave 4029aeeb6b80e843279738d6d35eee7a1adcd2ff 0 1606640582624 3 connected
3ceb11fe492f98432f124fd1dcb7b2bb1e769a96 127.0.0.1:17300@27300 master - 0 1606640580619 7 connected 0-5460
1179bb5f47e7f8221ba7917b5852f8064778e0db 127.0.0.1:17000@27000 master,fail - 1606640370074 1606640367068 1 disconnected
  1. 如果这时候主节点恢复呢?

和哨兵的模式一样,恢复之后也变为slave了。

集群模式优缺点:

优点:

  1. 无中心架构,数据按照slot分布在多个节点。
  2. 集群中的每个节点都是平等的关系,每个节点都保存各自的数据和整个集群的状态。每个节点都和其他所有节点连接,而且这些连接保持活跃,这样就保证了我们只需要连接集群中的任意一个节点,就可以获取到其他节点的数据。
  3. 可线性扩展到1000多个节点,节点可动态添加或删除
  4. 能够实现自动故障转移,节点之间通过gossip协议交换状态信息,用投票机制完成slave到master的角色转换

缺点:

  1. 客户端实现复杂,驱动要求实现Smart Client,缓存slots mapping信息并及时更新,提高了开发难度。目前仅JedisCluster相对成熟,异常处理还不完善,比如常见的“max redirect exception”
  2. 节点会因为某些原因发生阻塞(阻塞时间大于 cluster-node-timeout)被判断下线,这种failover是没有必要的
  3. 数据通过异步复制,不保证数据的强一致性
  4. slave充当“冷备”,不能缓解读压力
  5. 批量操作限制,目前只支持具有相同slot值的key执行批量操作,对mset、mget、sunion等操作支持不友好
  6. key事务操作支持有线,只支持多key在同一节点的事务操作,多key分布不同节点时无法使用事务功能
  7. 不支持多数据库空间,单机redis可以支持16个db,集群模式下只能使用一个,即db 0
Redis Cluster模式不建议使用pipeline和multi-keys操作,减少max redirect产生的场景。

cluster的相关疑问

为什么redis的槽要用 16384

img

值得高兴的是:这个问题作者出门回答了:

能理解作者意思的可以不用看下面的内容

地址:https://github.com/redis/redi...

The reason is:

Normal heartbeat packets carry the full configuration of a node, that can be replaced in an idempotent way with the old in order to update an old config. This means they contain the slots configuration for a node, in raw form, that uses 2k of space with16k slots, but would use a prohibitive 8k of space using 65k slots.
At the same time it is unlikely that Redis Cluster would scale to more than 1000 mater nodes because of other design tradeoffs.
So 16k was in the right range to ensure enough slots per master with a max of 1000 maters, but a small enough number to propagate the slot configuration as a raw bitmap easily. Note that in small clusters the bitmap would be hard to compress because when N is small the bitmap would have slots/N bits set that is a large percentage of bits set.
  1. 首先我们查看一下结构体,关于cluster的源代码:cluster.h

代码如下:

typedef struct {
    char sig[4];        /* Signature "RCmb" (Redis Cluster message bus). */
    uint32_t totlen;    /* Total length of this message */
    uint16_t ver;       /* Protocol version, currently set to 1. */
    uint16_t port;      /* TCP base port number. */
    uint16_t type;      /* Message type */
    uint16_t count;     /* Only used for some kind of messages. */
    uint64_t currentEpoch;  /* The epoch accordingly to the sending node. */
    uint64_t configEpoch;   /* The config epoch if it's a master, or the last
                               epoch advertised by its master if it is a
                               slave. */
    uint64_t offset;    /* Master replication offset if node is a master or
                           processed replication offset if node is a slave. */
    char sender[CLUSTER_NAMELEN]; /* Name of the sender node */
    unsigned char myslots[CLUSTER_SLOTS/8];
    char slaveof[CLUSTER_NAMELEN];
    char myip[NET_IP_STR_LEN];    /* Sender IP, if not all zeroed. */
    char notused1[34];  /* 34 bytes reserved for future usage. */
    uint16_t cport;      /* Sender TCP cluster bus port */
    uint16_t flags;      /* Sender node flags */
    unsigned char state; /* Cluster state from the POV of the sender */
    unsigned char mflags[3]; /* Message flags: CLUSTERMSG_FLAG[012]_... */
    union clusterMsgData data;
} clusterMsg;

集群节点之间的通信内容无非就是IP信息,请求头,请求内容,以及一些参数信息,这里着重看一下参数myslots[CLUSTER_SLOTS/8]

define CLUSTER_SLOTS 16384 这里就是16384的来源

在redis节点发送心跳包时需要把所有的槽放到这个心跳包里,以便让节点知道当前集群信息,16384=16k,在发送心跳包时使用char进行bitmap压缩后是2k(2 * 8 (8 bit) * 1024(1k) = 2K),也就是说使用2k的空间创建了16k的槽数。

虽然使用CRC16算法最多可以分配65535(2^16-1)个槽位,65535=65k,压缩后就是8k(8 * 8 (8 bit) * 1024(1k) = 8K),也就是说需要需要8k的心跳包,作者认为这样做不太值得;并且一般情况下一个redis集群不会有超过1000个master节点,所以16k的槽位是个比较合适的选择。

参考资料:

https://juejin.cn/post/684490...

https://juejin.cn/post/684490...

为什么Redis集群有16384个槽

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月22日

maven多模块和依赖冲突问题汇总记录

目录

前言:

今天学习和总结了一下maven的相关知识点,发现一些比较基础的东西居然也会忘记,这里对于一些日常工作中可能会遇到的问题进行了一下汇总。

idea怎么创建maven多module的项目

首先了解上面是多module?

一句话概括就是:一个父模块作为版本控制多个子模块,子模块负责接入到父模块当中作为整个项目的过程。

多Module管理项目的几种方式:

  1. 按照单模块拆分为多个子模块,比如将MVC三层架构拆分为 xxx-service,xxx-dao,xxx-model,不过这种方式个人感觉比较二,目前以业务模块拆分比较多,迁移到微服务比如用springcloude或者dubbo 的时候非常好用。
  2. 按照业务模块拆分,这种模式使用的比较多,也比较多见。

创建一个多module项目(idea2019.3.3版本)

创建一个父pom项目:

  1. 打开idea,选择create new project

  1. 选择maven项目,同时不选任何的预加载设置

  1. 父pom配置如下:

  1. 删除src 目录

创建子模块,引入到父pom里面

  1. 同样右击项目工程,选择new module,然后选择maven,这时候会出现父模块以及对应的子模块名称

  1. 此时在父模块里面发现引入了子模块的内容

子父模块的区别:

父pom.xml文件内容:

<groupId>org.zxd</groupId>
<artifactId>taglib</artifactId>
<packaging>pom</packaging>
<version>1.0.0</version>

<modules>
    <module>taglib-core</module>
</modules>

分为两个部分,一个部分是父pom的声明,包含gourpId,artifactId,打包方式必须是pom,因为使用了聚合模型,同时在父pom里面指定版本号,子模块不填写version会默认使用父pom的version号

<modules>
    <module>taglib-core</module>
</modules>

上面表示当前引入的子模块内容

子pom.xml文件内容:

<!-- 引用自父pom -->
<parent>
    <artifactId>taglib</artifactId>
    <groupId>org.zxd</groupId>
    <version>1.0.0</version>
</parent>
<!-- 打包方式为jar包 -->
<packaging>jar</packaging>
<modelVersion>4.0.0</modelVersion>

<artifactId>taglib-core</artifactId>
<version>1.0.0</version>

子模块之间进行互相的依赖

在下面的pom中可以在任意的子模块引入对应的父模块依赖

注意由于<parent>这个标签会递归继承,所以要注意子依赖不要和依赖引入不同版本的依赖,这样容易造成冲突

<dependency>
    <groupId>org.zxd</groupId>
    <artifactId>taglib-core</artifactId>
    <version>1.0.0</version>
    <!-- 这里需要注释掉编译的作用域 -->
    <!--<scope>compile</scope>-->
</dependency>

将上面的项目改造为spring-boot多模块项目:

改造父pom文件:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.4.0</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

parent指向springboot-started

子模块只需要引入父pom的内容

Spring boot maven plugin问题

在打包spring boot项目时,需要使用如下插件:

<plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
</plugin>

如果在用maven构建多模块项目时,不要将此插件放到parent pom中,否则如果有sub module不是spring boot应用,在打包时就会出错。只将该插件添加到是spring boot项目的子模块

MAVEN依赖冲突问题:

依赖的传递原则:

  1. 最短路径原则
  2. 最先声明原则

maven的依赖引入策略

最短路径原则:

我有下面两个依赖jar包,A和B,他们都引入了C这个依赖,这时候如果有如下的引用

A -> C(3.3)

B -> A(3.3)

B -> C(3.4)

此时如果把B打包,得到版本号是3.4,但是如果B去掉C的依赖,那就是走A->C的传递依赖,很简单

验证:

  1. 我假设我有一个web包引入了common-lang3,版本是3.4的版本
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.4</version>
</dependency>
  1. 此时又引入了一个公用包,里面也有这个引用:
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.3</version>
</dependency>
  1. 如果此时在本地引用3.4版本,那就是3.4的版本,否则就死3.3的版本,不管声明顺序谁先谁后

最先声明原则:

如果两个jar包的寻址路径一致,那么谁先优先声明,先引入谁

验证:

下面两个依赖分配对应两个module,引入模块的这个模块暂定为 C 模块。

<!-- 引入core包内容 -->
<dependency>
    <groupId>org.zxd</groupId>
    <artifactId>taglib-core</artifactId>
    <version>1.0.0</version>
    <!--            <scope>compile</scope>-->
</dependency>
<!-- 引入db包的内容 -->
<dependency>
    <groupId>org.zxd</groupId>
    <artifactId>taglib-db</artifactId>
    <version>1.0.0</version>
</dependency>

此时 taglib-core 中的依赖版本如下,暂定为 A 模块:

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>2.5.0</version>
</dependency>

taglib-db 中的依赖版本如下,暂定为 B 模块:

<dependency>
    <groupId>org.apache.curator</groupId>
    <artifactId>curator-client</artifactId>
    <version>4.0.1</version>
</dependency>

此时将整个web项目打包,可以看到web项目里面的版本如下:

可以很明显的看到如果引入关系是这样的:

C -> A,B

A -> curator-client 2.5

B -> curator-client 4.1

这样的链路最终打包出来的效果是 C -> A -> curator-client 2.5

这样也就造成了很多情况下我们编译运行时完全没有问题的,甚至有可能打包都是正常的,但是到最后运行的时候突然报错,要谨防这种依赖版本的问题,好在一般公司的项目都有经理负责控制版本依赖,这种错误算是低级错误,但是在如今框架满天飞的时代,依赖管理的版本控制问题依然需要注意!!!

如何解决依赖冲突的问题

锁定版本法

一般情况下我们会在父pom文件里面管理,可以使用<dependencyManagement>这个这个标签来管理所有子模块的版本依赖,子模块如果指定自己的版本,这里发现打出来的包依然是父pom指定的版本,版本管理使用如下:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-client</artifactId>
            <version>4.1.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>

锁定版本法可以打破2个依赖传递的原则,优先级为最高

版本锁定可以排除一些exclude标签,不同模块用不同版本的jar包本身也不符合规范,所以这种方式较为稳妥

什么情况下会出现Jar包冲突问题

只有高版本Jar包不向下兼容,或者新增了某些低版本没有的API才有可能导致这样的问题

如何查找和发现jar包冲突?

1. 利用idea的maven视图工具

直接使用一个图说明一下:

可以通过这个工具查看依赖在哪个模块重复引用,同时如果有冲突会显示红线,这个视图非常的直观,可以帮助依赖管理人员去处理依赖重复引用或者引用版本不一致的问题,进行<exclude>操作

2. Idea Maven Helper 插件

如何使用?

安装完成之后,随便找一个pom.xml文件,按照如下的图例提示进行操作,对于冲突的内容,右击exclude就可以排除依赖:

排除完依赖之后,点击左上角的Refresh UI 刷新一下UI的界面:

3. maven命令工具:

mvn dependency:tree -Dverbose,有时候如果我们没有idea的情况下,可以使用这个命令来处理,执行的结果参考如下:

PS: 此处一定不要省略-Dverbose参数,要不然是不会显示被忽略的包的
or:curator-client:jar:4.1.0:compile
[INFO] |  |  +- org.apache.zookeeper:zookeeper:jar:3.5.4-beta:compile
[INFO] |  |  |  +- commons-cli:commons-cli:jar:1.2:compile
[INFO] |  |  |  +- log4j:log4j:jar:1.2.17:compile
[INFO] |  |  |  +- org.apache.yetus:audience-annotations:jar:0.5.0:compile
[INFO] |  |  |  \- io.netty:netty:jar:3.10.6.Final:compile
[INFO] |  |  +- com.google.guava:guava:jar:20.0:compile
[INFO] |  |  \- org.slf4j:slf4j-api:jar:1.7.30:compile
[INFO] |  +- commons-codec:commons-codec:jar:1.15:compile
[INFO] |  +- commons-collections:commons-collections:jar:3.2.2:compile
[INFO] |  +- commons-beanutils:commons-beanutils:jar:1.9.4:compile
[INFO] |  +- commons-configuration:commons-configuration:jar:1.10:compile
[INFO] |  |  \- commons-lang:commons-lang:jar:2.6:compile

总体上来说还是比较直观的,非常方便和好用。

如何写一个干净依赖关系的POM文件

  • 尽量在父POM中定义<dependencyManagement>,来进行本项目一些依赖版本的管理,这样可以从很大程度上解决一定的冲突
  • 最少依赖jar包原则
  • 使用mvn dependency:analyze-only命令用于检测那些声明了但是没被使用的依赖,如有有一些是你自己声明的,那尽量去掉
  • 使用mvn dependency:analyze-duplicate命令用来分析重复定义的依赖,清理那些重复定义的依赖

dependency:analyze-only 命令

在idea - Terminal里面,可以看到对应的依赖被下载

执行完之后我的运行结果如下,这里报错的原因是打包时候默认去阿里云仓库寻找依赖,这里需要配置一下:

[ERROR] Failed to execute goal on project taglib-web: Could not resolve dependencies for project org.zxd:taglib-web:war:1.0.0: The following artifacts could not be resolved: org.zxd:
taglib-core:jar:1.0.0, org.zxd:taglib-db:jar:1.0.0: Failure to find org.zxd:taglib-core:jar:1.0.0 in http://maven.aliyun.com/nexus/content/repositories/central/ was cached in the loc
al repository, resolution will not be reattempted until the update interval of alimaven has elapsed or updates are forced -> [Help 1]

大致意思就是说再阿里云仓库找不到对应的依赖引入。

解决方式如下:

找到maven的安装路径下的apache-maven-3.6.3\conf下面的setting.xml,找到如下配置:

<!-- localRepository
   | The path to the local repository maven will use to store artifacts.
   |
   | Default: ${user.home}/.m2/repository
  <localRepository>/path/to/local/repo</localRepository>
  -->
<!-- 这里配置本地仓库的位置 -->
  <localRepository>D:\soft\apache-maven-3.6.3\rep</localRepository>

此时重新执行一下:dependency:analyze-only 命令

[INFO] --- maven-dependency-plugin:3.1.2:analyze-only (default-cli) @ taglib-web ---
[WARNING] Unused declared dependencies found:
[WARNING]    org.zxd:taglib-core:jar:1.0.0:compile
[WARNING]    org.apache.commons:commons-lang3:jar:3.4:compile
[WARNING]    org.springframework.boot:spring-boot-starter-web:jar:2.4.0:compile
[WARNING]    org.springframework.boot:spring-boot-starter-test:jar:2.4.0:test
[WARNING]    org.neo4j.driver:neo4j-java-driver:jar:1.5.0:compile
[WARNING]    commons-codec:commons-codec:jar:1.10:compile
[WARNING]    commons-collections:commons-collections:jar:3.2.2:compile
[WARNING]    commons-beanutils:commons-beanutils:jar:1.9.4:compile
[WARNING]    commons-configuration:commons-configuration:jar:1.10:compile
[WARNING]    commons-fileupload:commons-fileupload:jar:1.3:compile
[WARNING]    commons-logging:commons-logging:jar:1.2:compile
[WARNING]    org.apache.httpcomponents:httpclient:jar:4.4.1:compile
[WARNING]    org.apache.poi:poi-ooxml:jar:3.17:compile
[WARNING]    org.mybatis:mybatis:jar:3.4.0:compile
[WARNING]    org.mybatis:mybatis-spring:jar:1.3.0:compile
[WARNING]    com.github.pagehelper:pagehelper:jar:5.1.2:compile

mvn dependency:analyze-duplicate 命令

[INFO] No duplicate dependencies found in <dependencies/> or in <dependencyManagement/>

如果没有其他信息,代表没有重复依赖的引入

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月21日

redis学习 - redis 持久化

redis学习 - redis 持久化

无论面试和工作,持久化都是重点。

一般情况下,redis占用内存超过20GB以上的时候,必须考虑主从多redis实例进行数据同步和备份保证可用性。

rbd保存的文件都是 dump.rdb,都是配置文件当中的快照配置进行生成的。一般业务情况只需要用rdb即可。

aof默认是不开启的,因为aof非常容易产生大文件,虽然官方提供重写但是在文件体积过大的时候还是容易造成阻塞,谨慎考虑使用

rbd和aof在大数据量分别有各种不同情况的系统性能影响,具体使用何种解决策略需要根据系统资源以及业务的实际情况决定。

数据设计影响持久化:

https://szthanatos.github.io/...

为什么要持久化?

  1. 重用数据
  2. 防止系统故障备份重要数据

持久化的方式

  1. RDB 快照:将某一个时刻的所有数据写入到磁盘
  2. AOF(append-only file):将所有的命令写入到此判断。

默认情况:RDB,AOF需要手动开启

redis.conf持久化配置说明

redis.conf文件当中,存在如下的选项:

redis.conf当中RDB的相关配置

#是否开启rdb压缩 默认开启
rdbcompression yes
#代表900秒内有一次写入操作,就记录到rdb
save 900 1
# rdb的备份文件名称
dbfilename dump.rdb
# 表示备份文件存放位置
dir ./

redis.conf当中AOF的相关配置

# 是否开启aof,默认是关闭的
appendonly no
#aof的文件名称
appendfilename "appendonly.aof"
# no: don't fsync, just let the OS flush the data when it wants. Faster.
# always: fsync after every write to the append only log. Slow, Safest.
# everysec: fsync only one time every second. Compromise.
appendfsync everysec
# 在进行rewrite的时候不开启fsync,即不写入缓冲区,直接写入磁盘,这样会造成IO阻塞,但是最为安全,如果为yes表示写入缓冲区,写入的适合redis宕机会造成数据持久化问题(在linux的操作系统的默认设置下,最多会丢失30s的数据)
no-appendfsync-on-rewrite no
# 下面两个参数要配合使用,代表当redis内容大于64m同时扩容超过100%的时候会执行bgrewrite,进行持久化
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb

RDB

创建rdb快照的几种方式:

  1. 客户端向redis发送bgsave的命令(注意windows不支持bgsave),此时reids调用 fork 创建子进程,父进程继续处理,子进程将快照写入磁盘,父进程继续处理请求。
  2. 客户端发送save命令创建快照。注意这种方式会阻塞整个父进程。很少使用,特殊情况才使用。
  3. redis通过shutdown命令关闭服务器请求的时候,此时redis会停下所有工作执行一次save,阻塞所有客户端不再执行任何命令并且进行磁盘写入,写入完成关闭服务器。
  4. redis集群的时候,会发送sync 命令进行一次复制操作,如果主服务器没有执行或者刚刚执行完bgsave,则会进行bgsave。
  5. 执行flushall 命令

RDB快照的一些注意点:

  1. 只使用rdb的时候,如果创建快照的时候redis崩溃,redis会留存上一次备份快照,但是具体丢失多少数据由备份时间查看
  2. 只适用一些可以容忍一定数据丢失的系统,否则需要考虑aof持久化
  3. 在大数据量的场景下,特别是内存达到20GB以上的适合,一次同步大约要4-6秒

    1. 一种方式是用手动同步,在凌晨的适合进行手动阻塞同步,比BGSAVE快一些
一种解决方法:

通过日志记录来恢复中断的日志,来进行数据的恢复

如何通过修改配置来获得想要的持久化?

  1. 修改save参数,尽量在开发环境模拟线上环境设置save,过于频繁造成资源浪费,过于稀少有可能丢失大量数据
  2. 日志进行聚合计算,按照save进行计算最多会丢失多少时间的数据,判断容忍性,比如一小时可以设置 save 3600 1

RDB的优缺点对比:

优点:

  1. 适合大规模的数据恢复
  2. 如果数据不小心误删,可以及时恢复
  3. 恢复速度一般情况下快于aof

缺点:

  1. 需要一定的时间间隔,如果redis意外宕机,最后一次修改的数据就没有了,具体丢失多少数据需要看持久化策略
  2. fork进程的时候,会占用一定的内存空间,如果fork的内存过于庞大,可能导致秒级别的恢复时间
  3. 数据文件经过redis压缩,可读性较差

AOF(append only fail)

其实就是把我们的命令一条条记录下来,类似linux的history

默认是不开启的,需要手动开启,开启之后需要重启

如果aof文件错位了,可以用redis-check-aof 进行文件修复

文件同步:写入文件的时候,会发生三件事:

  1. file.write() 方法将文件存储到缓冲区
  2. file.flush() 将缓冲区的内容写入到硬盘
  3. sync 文件同步,阻塞直到写入硬盘为止

AOC的同步策略

选项同步频率
always每次命令都写入磁盘,严重降低redis速度
everysec每秒执行一次,显示将多个命令写入到磁盘
no操作系统决定,佛系

分析:

  1. 第一种对于固态的硬盘的伤害比较大,我们都知道固态的擦写次数的寿命是远远小于机械硬盘的,频繁的io是容易对固态造成欺骗认为一次擦写,导致本就寿命不长的固态变得更命短,基本不用,特殊情况下有可能用得到
  2. 第二种是默认的方式,也是推荐以及比较实用的方式,最多只会丢失一秒的数据,这种方式比较好的保证数据的备份可用,推荐使用
  3. 第三种对于CPU的压力是最小的,因为由系统决定,但是需要考虑能不能接受不定量的数据丢失,还有一个原因是硬盘将缓冲区刷新到硬盘不定时,所以不建议使用

重写和压缩AOF文件:

由于1秒一次同步在不断写入之后造成文件内容越来越大,同时同步速度也会变慢,为了解决这个问题,redis引入了bgrewriteaof命令来进行压缩,和bgsave创建快照类似,同样会有子进程拖垮的问题,同时会有大文件在重写的时候带来巨大的文件系统删除的压力,导致系统阻塞。

命令如下

bgrewriteaof

示例如下:

127.0.0.1:16379> BGREWRITEAOF
Background append only file rewriting started

参数控制:

auto-aof-rewrite-percentage:100

auto-aof-rewrite-min-size :64MB

这里案例配置代表当AOF大于64并且扩大了100%将处罚bgrewrite命令

redis aof的rewrite做了那些事?

  1. 对于一些冗余的命令进行清除
  2. 检测存在错误的命令,将错误命令下面的所有命令都进行清理,一般情况是末尾由于宕机没有执行完的一些命令清理。

aof的优缺点对比

优点:

  1. 从不同步,效率高
  2. 每秒同步一次,可能丢失一秒数据
  3. 每次修改都同步,文件完整性好

缺点:

  1. 相对于数据文件来说,aof远远大于rdb。修复速度慢一些
  2. 存在未知的bug,比如如果重写aof文件的时候突然中断,会有很多奇怪的现象

如何检查redis的性能瓶颈:

  1. redis-benchmark 官方推荐的性能测试工具,非常强大,具体的地址为:https://www.runoob.com/redis/...
  2. Redis-cli中调用slowlog get,作用是返回执行时间超过redis.conf中定义的持续时间的命令列表,注意这个时间仅仅是请求的处理时间,不包含网络通信的时间,默认值是一秒
redis.conf 当中对于慢日志的解释:

The following time is expressed in microseconds, so 1000000 is equivalent to one second. Note that a negative number disables the slow log, while a value of zero forces the logging of every command.

接下来的时间以微秒为单位,因此1000000等于一秒。 请注意,负数将禁用慢速日志记录,而零值将强制记录每个命令。(以微秒为单位)

slowlog-log-slower-than 10000

There is no limit to this length. Just be aware that it will consume memory. You can reclaim memory used by the slow log with SLOWLOG RESET.

该长度没有限制。 请注意,它将消耗内存。 您可以使用SLOWLOG RESET回收慢速日志使用的内存。(意思就是说超过128条之后的命令会被自动移除)

slowlog-max-len 128

可以用命令 SLOWLOG RESET 清楚慢日志占用的内存

127.0.0.1:16379> SLOWLOG reset
OK

==慢日志是存储在内存当中的,切记==

持久化性能建议

  • 因为RDB文件只用作后备用途,建议只在Slave上持久化RDB文件,而且只要15分钟备份一次就够了,只保留save 900 1这条规则。
  • 如果Enalbe AOF,好处是在最恶劣情况下也只会丢失不超过两秒数据,启动脚本较简单只load自己的AOF文件就可以了。代价一是带来了持续的IO,二是AOF rewrite的最后将rewrite过程中产生的新数据写到新文件造成的阻塞几乎是不可避免的。只要硬盘许可,应该尽量减少AOF rewrite的频率,AOF重写的基础大小默认值64M太小了,可以设到5G以上。默认超过原大小100%大小时重写可以改到适当的数值。
  • 如果不Enable AOF ,仅靠Master-Slave Replication 实现高可用性也可以。能省掉一大笔IO也减少了rewrite时带来的系统波动。代价是如果Master/Slave同时倒掉,会丢失十几分钟的数据,启动脚本也要比较两个Master/Slave中的RDB文件,载入较新的那个。新浪微博就选用了这种架构。

其他性能优化指南(强烈推荐):

https://szthanatos.github.io/...

总结对比rdb和aof:

RDBAOF
存储内容数据写操作日志
性能影响
恢复速度
存储空间
可读性
安全程度较低,保存频率低较高,保存频率高
默认开启
存储策略save 900 1:九百秒内一次修改即保存 save 300 10:三百秒内十次修改即保存 save 60 10000:六十秒内一万次修改即保存 允许自定义always:逐条保存 or everysec:每秒保存 or no:系统自己决定什么时候保存

其他拓展知识:

关于linux内核开启transparent_hugepage会带来的阻塞问题:

个人对于Linux学艺不精,就直接引用文章了,侵权请联系删除

Linux 关于Transparent Hugepages的介绍

简单说说THP——记一次数据库服务器阻塞的问题解决

官方解决aof和rdb对于性能问题的折中处理方式

  1. redis4.0之后有一个参数叫做:aof-use-rdb-preamble yes

参数解释如下:

# When rewriting the AOF file, Redis is able to use an RDB preamble in the
# AOF file for faster rewrites and recoveries. When this option is turned
# on the rewritten AOF file is composed of two different stanzas:
#
#   [RDB file][AOF tail]
#
# When loading, Redis recognizes that the AOF file starts with the "REDIS"
# string and loads the prefixed RDB file, then continues loading the AOF
# tail.
#重写AOF文件时,Redis可以在
#AOF文件可加快重写和恢复速度。 启用此选项时
重写的AOF文件上的#由两个不同的节组成:
#
#[RDB文件] [AOF尾巴]
#
#加载时,Redis会识别AOF文件以“ REDIS”开头
#字符串并加载带前缀的RDB文件,然后继续加载AOF
# 尾巴。

大致的内容就是说redis会将较早的部分内容转为RDB文件进行恢复,同时加入近期的数据为AOF文件

加载的时候先执行rdb文件的恢复,然后再加载aof命令

如何进行内存清理

redis4.0之后,可以通过将配置里的activedefrag设置为yes开启自动清理,或者通过memory purge命令手动清理。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月21日

我的Neo4j探索之旅 - 安装Apoc插件以及JAVA集成(二)

上一篇文章:

不知道为什么掘金我发不出文章,找不到那里违规了,上一篇文章我发布到思否了:

https://segmentfault.com/a/11...

如何安装neo4j - apoc 插件

有英语阅读能力建议参考官方文档:https://neo4j.com/developer/n...

1. 下载neo4j - apoc 插件

进入github : https://github.com/neo4j-cont...

找到和当前neo4j 匹配的版本, 我选择3.5.0.12 的版本

这里提供我的:

链接:https://pan.baidu.com/s/1Tb7f...
提取码:bzwh

2. 具体的安装步骤

  1. 下载好之后,放入到 D:\zxd\tool\neo4j-community-3.5.12-windows\neo4j-community-3.5.12\plugins 下面
  2. 执行neo4j stop,关闭neo4j 服务
  3. 进入 ~/conf 下面,找到neo4j.conf ~表示你的neo4j 安装位置
  4. 修改#dbms.security.procedures.whitelist=apoc.coll.*,apoc.load.* 在这一行的下面增加dbms.security.procedures.unrestricted=apoc.*的配置,安装apoc插件
下面的图看起来就像这样:

\#dbms.security.procedures.whitelist=apoc.coll.,apoc.load.
dbms.security.procedures.unrestricted=apoc.*

  1. 输入 neo4j start 启动neo4j 服务
  2. 在可视化界面,输入return apoc.version() ,如果报错说明没安装对,显示如下页面,证明apoc 插件安装成功

Neo4j 集成到java里面

1. 配置maven,加入依赖

<!-- neo4j 依赖包 -->
<dependency>
    <groupId>org.neo4j.driver</groupId>
    <artifactId>neo4j-java-driver</artifactId>
    <version>1.5.0</version>
</dependency>

2. 使用neo4j 集成java实战

这是之前实战的是用的方式,这里说下我大致的设计记录

需求:

  1. neo4j 实现插拔式配置,没有配置的情况下进行连接不会影响程序运行
  2. 所有的配置都需要放在application_setting.xml当中
  3. 如果连接neo4j 失败,不做处理

抽象接口

core 包里面, 设计公用开放接口。

    /**
     * 构建neo4j url地址
     * @return
     */
    String buildUrl();

    /**
     * 构建neo4j 用户名
     * @return
     */
    String buildUsername();

    /**
     * 构建neo4j 密码
     * @return
     */
    String buildPassword();

    /**
     * 构建neo4j 配置, 目前使用默认 配置
     * @return
     */
    Config buildConfig();

    /**
     * 是否开启自定义配置
     * @return
     */
    boolean enableCustomConfig();

    /**
     * 构建 Neo4j csv同步地址的前置
     * 默认为 file:///
     *
     * @return
     */
    String buildCvsPrefix();

    /**
     * 构建 neo4j csv同步的普通标签文件名
     * @return
     */
    String buildNormalTagSyncFileName();

    /**
     * 构建 neo4j csv同步的业务标签文件名
     * @return
     */
    String buildBusinessTagSyncFileName();

    /**
     * 构建 neo4j csv同步的用户标签文件名
     * @return
     */
    String buildUserTagSyncFileName();

    /**
     * 普通标签和业务标签的关联csv文件名称
     * @return
     */
    String buildNormalTagBizSyncFileName();

    /**
     * 普通标签和用户标签的关联csv文件名称
     * @return
     */
    String buildNormalTagUserSyncFileName();

    /**
     * 业务标签和用户标签的关联csv文件名称
     * @return
     */
    String buildBizTagUserSyncFileName();

    /**
     * 主题分类(业务)标签子父关联csv文件名称
     * @return
     */
    String buildBizChildSyncFileName();

    /**
     * 普通标签标签子父关联csv文件名称
     * TODO: 目前普通标签没有子父关联关系,后续需要拓展请开放此接口 by zhaoxudong
     * @return
     */
//    String buildNormalChildSyncFileName();

    /**
     * 用户标签的子父关联csv文件名称
     * @return
     */
    String buildUserChildSyncFileName();

具体实现

maven的其他项目工程,只要实现了上面的接口,就可以根据自己的设定去设置如何读取配置,下面给出案例

@Override
    public String buildUrl() {
        return Setter.getString("neo4j.address");
    }

    @Override
    public String buildUsername() {
        return Setter.getString("neo4j.username");
    }

    @Override
    public String buildPassword() {
        return Setter.getString("neo4j.password");
    }

    @Override
    public Config buildConfig() {
        return Config.defaultConfig();
    }

    @Override
    public boolean enableCustomConfig() {
        return Setter.getBoolean("neo4j.enablecustomconfig");
    }

    @Override
    public String buildCvsPrefix() {
        return Setter.getString("neo4j.datasyncprefix");
    }

    @Override
    public String buildNormalTagSyncFileName() {
        return Setter.getString("neo4j.normaltagsyncfilename");
    }

    @Override
    public String buildBusinessTagSyncFileName() {
        return Setter.getString("neo4j.businesstagsyncfilename");
    }

    @Override
    public String buildUserTagSyncFileName() {
        return Setter.getString("neo4j.usertagsyncfilename");
    }

    @Override
    public String buildNormalTagBizSyncFileName() {
        return Setter.getString("neo4j.normalbizsyncfilename");
    }

    @Override
    public String buildNormalTagUserSyncFileName() {
        return Setter.getString("neo4j.normalusersyncfilename");
    }

    @Override
    public String buildBizTagUserSyncFileName() {
        return Setter.getString("neo4j.bizusersyncfilename");
    }

    @Override
    public String buildBizChildSyncFileName() {
        return Setter.getString("neo4j.bizrelsyncfilename");
    }

    @Override
    public String buildUserChildSyncFileName() {
        return Setter.getString("neo4j.userrelsyncfilename");
    }

结合到neo4j 连接

  1. 根据上面两个步骤, ssm项目启动之后,会自动装载BEAN, 装载之后,只要在具体的程序里面,拿取配置即可(具体的配置获取实现可以由自己实现)
  2. 根据上面的方法使用java拿到xml配置之后,就实现了下面的方式实现neo4j 的数据连接
/**
     * 连接图数据库
     * @return
     */
private void createDriver(Neo4jConfigBuilder builder) {
    if(driver == null){
        try{
            driver =  GraphDatabase.driver(builder.buildUrl(), AuthTokens.basic(builder.buildUsername(), builder.buildPassword()));
        }catch(Exception e){
            driver = null;
            LOGGER.debug("无法建立图数据库连接,请检查图Neo4j 服务是否启动");
            throw new RuntimeException("无法建立图数据库连接,请检查图Neo4j 服务是否启动");
        }
    }
}

具体开发

 /**
     * 执行添加cql
     *
     * @param cql 查询语句
     */
    @Override
    public StatementResult run(Neo4jConfigBuilder builder,String cql) {
        createDriver(builder);
        Session session = driver.session();
        StatementResult run = null;
        try {
            run = session.run(cql);
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            if(session!=null){
                session.close();
            }
        }
        return run;
    }

    @Override
    public StatementResult run(Neo4jConfigBuilder builder,String cql, Object... objects) {
        createDriver(builder);
        Session session = driver.session();
        StatementResult run = null;
        try {
            run = session.run(cql, Values.parameters(objects));
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            if(session!=null){
                session.close();
            }
        }
        return run;
    }

曾经开发的时候查找的一些博客记录:

NEO4J的安装配置及使用总结

neo4j︱neo4j批量导入neo4j-import (五)

neo4j - 查询效率的几种优化思路

Neo4j如何对大量数据(千万节点及以上)进行初始化

关于Neo4j和Cypher批量更新和批量插入优化的5个建议

Neo4j的查询速度为何这么慢?这能商用吗?

Neo4j清空所有数据

Neo4j安装APOC和图算法Neo.ClientError.Procedure.ProcedureRegistrationFailed?

官方网站对于Apoc插件的介绍

neo4j cypher 语言的语法(非常非常重要):

neo4j--Cypher语法练习(START、CREATE、MERGE)

Neo4j中使用Cypher进行大批量节点删除的优化

thinbug 我的很多前端疑难杂症就是靠这网站解决的

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月11日

如何使用java代码导出word

前言:

导出word的需求其实在日常工作中用到的地方还不少,于是想写一篇文章好好记录一下,在导出之前,需要了解一下关于浏览器如何处理servlet的后台数据。具体可以了解一下http通信下载行为在servlet的实现。

==导出的工具类代码来源于网络,如有侵权可以联系我删除文章==

个人使用==ftl==作为word导出模板引擎,有很多模板引擎可以选,个人经过查阅资料发现ftl用的比较多,所以选择这一种

<!-- more -->

码云地址:

文章牵扯代码比较多,如果要看具操作可以查看我自己瞎弄的一个码云地址:

https://gitee.com/lazyTimes/i...

效果演示:

给了一个测试页面,临时写了一些脚本,可以作为参考(后续会贴Html代码进去)

点击提交,导出内容, 导出word报告

导出之后,打开word内容为:

实现步骤 - 制作word模板

第一步 新建word,制作成果样板

将需要导出word的内容,先粘贴到一个新建的word文件里面

第二步 转存格式 -> xml

选择文件“另存为”,将格式设置为xml格式

第三步 格式化文件

将文件放到idea或者支持格式化的软件里面,进行格式化,保存:

注意占位符要匹配

第四步:模板数据替换占位符

在word页面将需要导入数据的地方,替换占位符

需要注意内容处理的时候: ${ filename} 有可能被切割为多个部分,我们需要把多个切割部分,改为下面的样式

一定记得所有的改动之后,马上打开xml格式的word,确认是不是改崩了

上面的步骤完成,说明有一个word模板做好了

第五步:制作ftl文件,word模板成型

在项目里面新建一个ftl文件,同时需要在工具类中配置,同时把做好站位符操作的xml内容贴进去

代码实现 - 导出代码

  1. 工具类的配置如下:

WordGeneratorUtil.java

/**
     * 模板常量类配置
     */
public static final class FreemarkerTemplate {
    public static final String REPORT = "report";
    public static final String REC_RECOMMEND = "recRecommend";
    // 增加你的模板文件名称:

}

在静态的代码块里面,需要注入对应的模板配置

// 注意初始化要载入对应模板
allTemplates.put(FreemarkerTemplate.REPORT, configuration.getTemplate(FreemarkerTemplate.REPORT + ".ftl"));
allTemplates.put(FreemarkerTemplate.REC_RECOMMEND,configuration.getTemplate(FreemarkerTemplate.REC_RECOMMEND + ".ftl"));
  1. 在配置完成之后,导出的时候就可以找到对应的文件了
  2. 建立一个通用的导出方法:
/**
     * 创建doc 文档
     * dataMap 数据,需要对应模板的占位符,否则会出错
     * @param dataMap 数据
     * @param wordName  word 报表的名称
     * @param freemarkerTemplateName  指定需要使用哪个freemarker模板
     * @return
     */
public static File createDoc(String freemarkerTemplateName, String wordName, Map<String, String> dataMap) {
    try {
        File f = new File(wordName);
        Template t = allTemplates.get(freemarkerTemplateName);
        // 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
        Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8);
        t.process(dataMap, w);
        w.close();
        return f;
    } catch (Exception ex) {
        ex.printStackTrace();
        throw new RuntimeException("生成word文档失败");
    }
}

工具类完整代码:

package com.zxd.interview.export;


import freemarker.template.Configuration;
import freemarker.template.Template;
import org.springframework.util.CollectionUtils;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;

/**
 * <p>
 * 从网络上根据资料找到的一个工具类
 * 主要以freemarker 为核心的模板生成word文档的工具类
 * 这里默认配置了固定路径
 * 需要根据路径取到对应模板
 * 请求参数需要设置对应的模板名称
 * @author 
 * @className: WordGeneratorUtils
 * @description: 文档生成工具类
 * </p>
 * version: V1.0.0
 */
public final class WordGeneratorUtil {
    private static Configuration configuration = null;
    private static Map<String, Template> allTemplates = null;
    private static final String TEMPLATE_URL = "/templates";

    /**
     * 模板常量类配置
     */
    public static final class FreemarkerTemplate {
        public static final String Test = "test";
        public static final String REPORT = "report";
        public static final String REC_RECOMMEND = "recRecommend";

    }

    static {
        configuration = new Configuration(Configuration.VERSION_2_3_28);
        configuration.setDefaultEncoding("utf-8");
        configuration.setClassForTemplateLoading(WordGeneratorUtil.class, TEMPLATE_URL);
        allTemplates = new HashMap(4);
        try {
            // 注意初始化要载入对应模板
            allTemplates.put(FreemarkerTemplate.Test, configuration.getTemplate(FreemarkerTemplate.Test + ".ftl"));
            allTemplates.put(FreemarkerTemplate.REPORT, configuration.getTemplate(FreemarkerTemplate.REPORT + ".ftl"));
            allTemplates.put(FreemarkerTemplate.REC_RECOMMEND, configuration.getTemplate(FreemarkerTemplate.REC_RECOMMEND + ".ftl"));
        } catch (IOException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        }
    }

    private WordGeneratorUtil() {
        throw new AssertionError();
    }

    /**
     * 创建doc 文档
     * dataMap 数据,需要对应模板的占位符,否则会出错
     * @param dataMap 数据
     * @param wordName  word 报表的名称
     * @param freemarkerTemplateName  指定需要使用哪个freemarker模板
     * @return
     */
    public static File createDoc(String freemarkerTemplateName, String wordName, Map<String, String> dataMap) {
        try {
            File f = new File(wordName);
            Template t = allTemplates.get(freemarkerTemplateName);
            // 这个地方不能使用FileWriter因为需要指定编码类型否则生成的Word文档会因为有无法识别的编码而无法打开
            Writer w = new OutputStreamWriter(new FileOutputStream(f), StandardCharsets.UTF_8);
            t.process(dataMap, w);
            w.close();
            return f;
        } catch (Exception ex) {
            ex.printStackTrace();
            throw new RuntimeException("生成word文档失败");
        }
    }


}

调用层:

  1. 在业务层,将需要导出的数据,根据占位符的i信息进行赋值,注意不能漏,否则导出之后的文件会打不开
@Override
public File exportQualityStep4Word(WordReportDTO exportWordRequest) {
    Map<String, String> datas = new HashMap(QualityConstants.HASH_MAP_INIT_VALUE);
    //主标题
    datas.put("schoolName", exportWordRequest.getSchoolName());
    datas.put("title1", exportWordRequest.getBaseSituation());
    datas.put("title2", exportWordRequest.getLearningEnvRec());
    datas.put("title3", exportWordRequest.getLearningEnvPro());
    datas.put("title4", exportWordRequest.getDayLifeRec());
    datas.put("title5", exportWordRequest.getDayLifePro());
    datas.put("title6", exportWordRequest.getLearningActivityRec());
    datas.put("title7", exportWordRequest.getLearningActivityPro());
    datas.put("title8", exportWordRequest.getDevRecommend());

    datas.put("base64_1", exportWordRequest.getBase64_1());
    datas.put("base64_2", exportWordRequest.getBase64_2());
    datas.put("base64_3", exportWordRequest.getBase64_3());
    datas.put("base64_4", exportWordRequest.getBase64_4());
    datas.put("base64_5", exportWordRequest.getBase64_5());
    datas.put("base64_6", exportWordRequest.getBase64_6());


    //导出
    return WordGeneratorUtil.createDoc(WordGeneratorUtil.FreemarkerTemplate.REPORT,
                                       exportWordRequest.getWordName(),
                                       datas);
}
  1. 下面是生成报表导出的基本操作,可以在用到的地方复制过去改动即可
/**
 * 生成报告的导出报表操作
 *
 * @param request           request
 * @param response          响应数据
 * @param exportWordRequest 导出dto
 */
@PostMapping("/quality/exportword")
@ResponseBody
public void povertyExportWord(HttpServletRequest request, HttpServletResponse response,
                              WordReportDTO exportWordRequest) {

    File file = qualityReportService.exportQualityStep4Word(exportWordRequest);

    InputStream fin = null;
    OutputStream out = null;
    try {
        // 调用工具类WordGeneratorUtils的createDoc方法生成Word文档
        fin = new FileInputStream(file);

        response.setCharacterEncoding(QualityConstants.UTF_8);
        response.setContentType(QualityConstants.CONTENT_TYPE_WORD);
        // 设置浏览器以下载的方式处理该文件
        // 设置文件名编码解决文件名乱码问题
        //获得请求头中的User-Agent
        String filename = exportWordRequest.getWordName();
        String agent = request.getHeader(QualityConstants.USER_AGENT);
        String filenameEncoder = "";
        // 根据不同的浏览器进行不同的判断
        if (agent.contains(QualityConstants.MSIE)) {
            // IE浏览器
            filenameEncoder = URLEncoder.encode(filename, QualityConstants.UTF_8);
            filenameEncoder = filenameEncoder.replace("+", " ");
        } else if (agent.contains(QualityConstants.FIREFOX)) {
            // 火狐浏览器
            BASE64Encoder base64Encoder = new BASE64Encoder();
            filenameEncoder = "=?utf-8?B?" + base64Encoder.encode(filename.getBytes(QualityConstants.UTF_8)) + "?=";
        } else {
            // 其它浏览器
            filenameEncoder = URLEncoder.encode(filename, QualityConstants.UTF_8);
        }
        response.setHeader(QualityConstants.ACCESS_CONTROL_ALLOW_ORIGIN, "*");//所有域都可以跨
        response.setHeader(QualityConstants.CONTENT_TYPE, QualityConstants.CONTENT_TYPE_STEAM);//二进制  流文件
        response.setHeader(QualityConstants.CONTENT_DISPOSITION, "attachment;filename=" + filenameEncoder + ".doc");//下载及其文件名
        response.setHeader(QualityConstants.CONNECTION, QualityConstants.CLOSE);//关闭请求头连接
        //设置文件在浏览器打开还是下载
        response.setContentType(QualityConstants.CONTENT_TYPE_DOWNLOAD);
        out = response.getOutputStream();
        byte[] buffer = new byte[QualityConstants.BYTE_512];
        int bytesToRead = QualityConstants.NUM_MINUS_1;
        // 通过循环将读入的Word文件的内容输出到浏览器中
        while ((bytesToRead = fin.read(buffer)) != QualityConstants.NUM_MINUS_1) {
            out.write(buffer, QualityConstants.NUM_ZERO, bytesToRead);
        }

    } catch (Exception e) {
        throw new RuntimeException(QualityConstants.FARIURE_EXPORT, e);
    } finally {
        try {
            if (fin != null) {
                fin.close();
            }
            if (out != null) {
                out.close();
            }
            if (file != null) {
                file.delete();
            }
        } catch (IOException e) {
            throw new RuntimeException(QualityConstants.FARIURE_EXPORT, e);
        }
    }

}

导出实体dto

下面写了一个导出的实体dto,实体对象可以自己定制:

package com.zxd.interview.dto;

/**
 * 测试使用的dto,用于封装导出word的对象
 *
 * @author zhaoxudong
 * @version 1.0
 * @date 2020/11/7 23:37
 */
public class TestReportDTO {

    /**
     * 测试
     */
    private String test0;

    /**
     * 测试
     */
    private String test1;

    /**
     * 测试
     */
    private String test2;

    /**
     * 测试
     */
    private String test4;

    /**
     * 测试
     */
    private String test5;

    /**
     * 测试
     */
    private String test6;

    /**
     * 报告名称
     */
    private String wordName;

    public String getTest0() {
        return test0;
    }

    public void setTest0(String test0) {
        this.test0 = test0;
    }

    public String getTest1() {
        return test1;
    }

    public void setTest1(String test1) {
        this.test1 = test1;
    }

    public String getTest2() {
        return test2;
    }

    public void setTest2(String test2) {
        this.test2 = test2;
    }

    public String getTest4() {
        return test4;
    }

    public void setTest4(String test4) {
        this.test4 = test4;
    }

    public String getTest5() {
        return test5;
    }

    public void setTest5(String test5) {
        this.test5 = test5;
    }

    public String getTest6() {
        return test6;
    }

    public void setTest6(String test6) {
        this.test6 = test6;
    }

    public String getWordName() {
        return wordName;
    }

    public void setWordName(String wordName) {
        this.wordName = wordName;
    }


    @Override
    public String toString() {
        return "TestReportDTO{" +
                "test0='" + test0 + '\'' +
                ", test1='" + test1 + '\'' +
                ", test2='" + test2 + '\'' +
                ", test4='" + test4 + '\'' +
                ", test5='" + test5 + '\'' +
                ", test6='" + test6 + '\'' +
                '}';
    }
}

常量配置模块:

个人很不喜欢硬编码这东西,又丑又难看,所以很多东西会用不可变对象替代.

package com.zxd.interview.constant;

/**
 * 常量配置类
 *
 * @author zhouhui
 */
public class QualityConstants {

    /**
     * 质量检测 的督导事项id
     */
    public static final int EVENTID = 12;

    /**
     * 数字0
     */
    public static final int NUM_ZERO = 0;
    /**
     * 数字1
     */
    public static final int NUM_ONE = 1;

    /**
     * 数字2
     */
    public static final int NUM_TWO = 2;
    /**
     * 数字-1
     */
    public static final int NUM_MINUS_1 = -1;
    /**
     * 字节大小512
     */
    public static final int BYTE_512 = 512;
    /**
     * 500错误编码
     */
    public static final int CODE_500 = 500;
    /**
     * 500错误提示信息 - 状态非法
     */
    public static final String CODE_500_MSG_1 = "状态非法!";
    /**
     * 500错误提示信息 - 非督导用户不允许查看质量检测记录
     */
    public static final String CODE_500_MSG_2 = "非督导用户不允许查看质量检测记录!";
    /**
     * 500错误提示信息 - 这条质量监测已经完成!无法修改
     */
    public static final String CODE_500_MSG_3 = "这条质量监测已经完成!无法修改!";
    /**
     * 500错误提示信息 - 提交失败,材料上传不能为空
     */
    public static final String CODE_500_MSG_4 = "提交失败,材料上传不能为空";
    /**
     * 500错误提示信息 - 提交失败,请稍后重试或联系管理员
     */
    public static final String CODE_500_MSG_5 = "提交失败,请稍后重试或联系管理员!";
    /**
     * 500错误提示信息 - 提交失败,意见反馈不能为空
     */
    public static final String CODE_500_MSG_6 = "提交失败,意见反馈不能为空!";
    /**
     * 405错误编码
     */
    public static final int CODE_405 = 405;
    /**
     * 405错误提示信息 - 该信息只允许督导查看
     */
    public static final String CODE_405_MSG_1 = "该信息只允许督导查看!";
    /**
     * 200成功编码
     */
    public static final int CODE_200 = 200;
    /**
     * 200成功提示信息 - 该信息只允许督导查看
     */
    public static final String CODE_200_MSG_1 = "提交成功!";

    /**
     * 错误提示信息 - 尚未选择记录
     */
    public static final String DELETE_FAIRURE_MSG = "删除失败,尚未选择记录!";
    /**
     * 错误提示信息 - 尚未选择记录
     */
    public static final String NO_RECORD_SELECTED = "尚未选择记录!";
    /**
     * 字符编码utf-8
     */
    public static final String UTF_8 = "utf-8";
    /**
     * 默认pid
     */
    public static final int PID = 0;
    /**
     * 默认层级
     */
    public static final int DEFUALT_LAYER = 1;

    /**
     * 不适当最低得分
     */
    public static final Integer MIN_SCORE = 1;
    /**
     * 优秀最高得分
     */
    public static final Integer MAX_SCORE = 7;

    /**
     * map的hash初始值
     */
    public static final int HASH_MAP_INIT_VALUE = 32;

    /**
     * 全园平均分
     */
    public final static String WHOLE_AVERAGE = "全园平均分";
    /**
     * 查询失败
     */
    public final static String QUERY_FAIRURE = "查询失败";
    /**
     * 操作成功
     */
    public final static String SUCCESS_MSG = "操作成功!";
    /**
     * 操作失败
     */
    public final static String FARIURE_MSG = "操作失败!";
    /**
     * 导出失败
     */
    public final static String FARIURE_EXPORT = "导出失败!";

    /**
     * 请求头 - 文档
     */
    public final static String CONTENT_TYPE_WORD = "application/msword";
    /**
     * 请求头 - 下载
     */
    public final static String CONTENT_TYPE_DOWNLOAD = "application/x-download";
    /**
     * 请求头 - 二进制文件
     */
    public final static String CONTENT_TYPE_STEAM = "application/octet-stream;charset=UTF-8";
    /**
     * 请求头
     */
    public final static String USER_AGENT = "User-Agent";
    /**
     * 请求头
     */
    public final static String CONTENT_TYPE = "Content-Type";
    /**
     * 连接
     */
    public final static String CONNECTION = "Connection";
    /**
     * 关闭连接
     */
    public final static String CLOSE = "close";
    /**
     * 连接
     */
    public final static String ACCESS_CONTROL_ALLOW_ORIGIN = "Access-Control-Allow-Origin";
    /**
     * 连接
     */
    public final static String CONTENT_DISPOSITION = "Content-Disposition";

    /**
     * 浏览器 - ie
     */
    public final static String MSIE = "MSIE";
    /**
     * 浏览器 - Firefox
     */
    public final static String FIREFOX = "Firefox";

    /**
     * 填写报告的step
     */
    public final static String MODULE_STEP3_REPORT = "qualityreport";

    /**
     * 督导下园核实的材料
     */
    public final static String MODULE_STEP1_MATERIAL = "qualitymetrail";

    /**
     * 数字3
     */
    public final static int NUM_3 = 3;
    /**
     * 数字4
     */
    public final static int NUM_4 = 4;

    /**
     * 数字5
     */
    public final static int NUM_5 = 5;
    /**
     * 数字6
     */
    public final static int NUM_6 = 6;
    /**
     * 数字7
     */
    public final static int NUM_7 = 7;
    /**
     * 数字8
     */
    public final static int NUM_8 = 8;
    /**
     * 数字9
     */
    public final static int NUM_9 = 9;
    /**
     * 数字10
     */
    public final static int NUM_10 = 10;
    /**
     * 数字11
     */
    public final static int NUM_11 = 11;

    /**
     * 数字12
     */
    public final static int NUM_12 = 12;
    /**
     * 数字13
     */
    public final static int NUM_13 = 13;
    /**
     * 数字14
     */
    public final static int NUM_14 = 14;
    /**
     * 数字15
     */
    public final static int NUM_15 = 15;
    /**
     * 数字16
     */
    public final static int NUM_16 = 16;
    /**
     * 数字17
     */
    public final static int NUM_17 = 17;
    /**
     * 数字18
     */
    public final static int NUM_18 = 18;
    /**
     * 数字19
     */
    public final static int NUM_19 = 19;
    /**
     * 数字20
     */
    public final static int NUM_20 = 20;

    /**
     * 格式化数字
     */
    public final static String DECIMAL_Format = "######.00";

}

页面层处理:

前端增加一个form提交,使用form提交表单数据,实现word导出功能:

(注意使用的模板引擎是thymeleaf)

html代码:

<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">

<head>
    <meta charset="UTF-8">
    <title>Title</title>

</head>
<body>
<div class="container">
    <div id="vue1" class="row">
<!--        <form @submit.prevent="submit">-->
<!--            <div>-->
<!--                导出word名称:<input class="form-control" type="text" v-model="student.wordName">-->
<!--            </div>-->

<!--            <div>-->
<!--                test0:<input type="text" class="form-control" v-model="student.test0">-->
<!--                test1:<input type="text" class="form-control" v-model="student.test1">-->
<!--                test2:<input type="text" class="form-control" v-model="student.test2">-->
<!--                test4:<input type="text" class="form-control" v-model="student.test4">-->
<!--                test5:<input type="text" class="form-control" v-model="student.test5">-->
<!--                test6:<input type="text" class="form-control" v-model="student.test6">-->
<!--            </div>-->

<!--            <input type="submit" class="btn btn-danger" value="提交">-->
<!--        </form>-->


        <form method="post" action="/quality/exportword">
            <div>
                导出word名称:<input class="form-control" type="text" name="wordName">
            </div>

            <div>
                test0:<input type="text" class="form-control" name="test0">
                test1:<input type="text" class="form-control" name="test1">
                test2:<input type="text" class="form-control" name="test2">
                test4:<input type="text" class="form-control" name="test4">
                test5:<input type="text" class="form-control" name="test5">
                test6:<input type="text" class="form-control" name="test6">
            </div>

            <input type="submit" class="btn btn-danger" value="提交">
        </form>

    </div>
</div>

</body>
<!-- 开发环境版本,包含了有帮助的命令行警告 -->
<script data-original="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script data-original="https://cdn.staticfile.org/vue-resource/1.5.1/vue-resource.min.js"></script>
<script th:data-original="@{/js/exportword.js}"></script>
</html>

js代码

使用js代码处理form表单提交,使用了jquery进行导出,其实一直不太懂前端怎么导出后台产生的二进制流,做法挺多,下次写一篇文章好好汇总一下几种用法。

var v1 = new Vue({
    el: '#vue1',
    data: {
        counter: 0,
        student: {
            test0:'',
            test1:'',
            test2:'',
            test3:'',
            test4:'',
            test5:'',
            test6:'',
            wordName: '',

        }
    },
    methods: {
        test: function () {
            console.log(this.counter);
        },
        submit() {
            console.log(this.student);
            var url = '/quality/exportword';
            var formData = JSON.stringify(this.student); // this指向这个VUE实例 data默认绑定在实例下的。所以直接this.student就是要提交的数据
            this.$http.post(url, formData).then(function (data) {
                console.log(data);
                let blob = new Blob([data.data],{ type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=utf-8'});
                let objectUrl = URL.createObjectURL(blob);
                window.location.href = objectUrl;
            }).catch(function () {
                console.log('test');
            });
        }
    }
})

结尾:

个人水平一般,希望通过这篇文章可以帮到读者,有错误的地方欢迎指点,看到会及时改成,谢谢!

前段时间忙于面试找到新的地方工作了,等工作安定之后,会继续深耕博客和技术栈。

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月4日

我的Neo4j探索之旅 - 安装Apoc插件以及JAVA集成(二)

上一篇文章:

不知道为什么掘金我发不出文章,找不到那里违规了,上一篇文章我发布到思否了:

https://segmentfault.com/a/11...

如何安装neo4j - apoc 插件

有英语阅读能力建议参考官方文档:https://neo4j.com/developer/n...

1. 下载neo4j - apoc 插件

进入github : https://github.com/neo4j-cont...

找到和当前neo4j 匹配的版本, 我选择3.5.0.12 的版本

这里提供我的:

链接:https://pan.baidu.com/s/1Tb7f...
提取码:bzwh

2. 具体的安装步骤

  1. 下载好之后,放入到 D:\\zxd\\tool\\neo4j-community-3.5.12-windows\\neo4j-community-3.5.12\\plugins 下面
  2. 执行neo4j stop,关闭neo4j 服务
  3. 进入 ~/conf 下面,找到neo4j.conf ~表示你的neo4j 安装位置
  4. 修改#dbms.security.procedures.whitelist=apoc.coll.*,apoc.load.* 在这一行的下面增加dbms.security.procedures.unrestricted=apoc.*的配置,安装apoc插件
下面的图看起来就像这样:

\#dbms.security.procedures.whitelist=apoc.coll.,apoc.load.
dbms.security.procedures.unrestricted=apoc.*

  1. 输入 neo4j start 启动neo4j 服务
  2. 在可视化界面,输入return apoc.version() ,如果报错说明没安装对,显示如下页面,证明apoc 插件安装成功

Neo4j 集成到java里面

1. 配置maven,加入依赖

<!-- neo4j 依赖包 -->
<dependency>
    <groupId>org.neo4j.driver</groupId>
    <artifactId>neo4j-java-driver</artifactId>
    <version>1.5.0</version>
</dependency>

2. 使用neo4j 集成java实战

这是之前实战的是用的方式,这里说下我大致的设计记录

需求:

  1. neo4j 实现插拔式配置,没有配置的情况下进行连接不会影响程序运行
  2. 所有的配置都需要放在application_setting.xml当中
  3. 如果连接neo4j 失败,不做处理

抽象接口

core 包里面, 设计公用开放接口。

    /**
     * 构建neo4j url地址
     * @return
     */
    String buildUrl();

    /**
     * 构建neo4j 用户名
     * @return
     */
    String buildUsername();

    /**
     * 构建neo4j 密码
     * @return
     */
    String buildPassword();

    /**
     * 构建neo4j 配置, 目前使用默认 配置
     * @return
     */
    Config buildConfig();

    /**
     * 是否开启自定义配置
     * @return
     */
    boolean enableCustomConfig();

    /**
     * 构建 Neo4j csv同步地址的前置
     * 默认为 file:///
     *
     * @return
     */
    String buildCvsPrefix();

    /**
     * 构建 neo4j csv同步的普通标签文件名
     * @return
     */
    String buildNormalTagSyncFileName();

    /**
     * 构建 neo4j csv同步的业务标签文件名
     * @return
     */
    String buildBusinessTagSyncFileName();

    /**
     * 构建 neo4j csv同步的用户标签文件名
     * @return
     */
    String buildUserTagSyncFileName();

    /**
     * 普通标签和业务标签的关联csv文件名称
     * @return
     */
    String buildNormalTagBizSyncFileName();

    /**
     * 普通标签和用户标签的关联csv文件名称
     * @return
     */
    String buildNormalTagUserSyncFileName();

    /**
     * 业务标签和用户标签的关联csv文件名称
     * @return
     */
    String buildBizTagUserSyncFileName();

    /**
     * 主题分类(业务)标签子父关联csv文件名称
     * @return
     */
    String buildBizChildSyncFileName();

    /**
     * 普通标签标签子父关联csv文件名称
     * TODO: 目前普通标签没有子父关联关系,后续需要拓展请开放此接口 by zhaoxudong
     * @return
     */
//    String buildNormalChildSyncFileName();

    /**
     * 用户标签的子父关联csv文件名称
     * @return
     */
    String buildUserChildSyncFileName();

具体实现

maven的其他项目工程,只要实现了上面的接口,就可以根据自己的设定去设置如何读取配置,下面给出案例

@Override
    public String buildUrl() {
        return Setter.getString("neo4j.address");
    }

    @Override
    public String buildUsername() {
        return Setter.getString("neo4j.username");
    }

    @Override
    public String buildPassword() {
        return Setter.getString("neo4j.password");
    }

    @Override
    public Config buildConfig() {
        return Config.defaultConfig();
    }

    @Override
    public boolean enableCustomConfig() {
        return Setter.getBoolean("neo4j.enablecustomconfig");
    }

    @Override
    public String buildCvsPrefix() {
        return Setter.getString("neo4j.datasyncprefix");
    }

    @Override
    public String buildNormalTagSyncFileName() {
        return Setter.getString("neo4j.normaltagsyncfilename");
    }

    @Override
    public String buildBusinessTagSyncFileName() {
        return Setter.getString("neo4j.businesstagsyncfilename");
    }

    @Override
    public String buildUserTagSyncFileName() {
        return Setter.getString("neo4j.usertagsyncfilename");
    }

    @Override
    public String buildNormalTagBizSyncFileName() {
        return Setter.getString("neo4j.normalbizsyncfilename");
    }

    @Override
    public String buildNormalTagUserSyncFileName() {
        return Setter.getString("neo4j.normalusersyncfilename");
    }

    @Override
    public String buildBizTagUserSyncFileName() {
        return Setter.getString("neo4j.bizusersyncfilename");
    }

    @Override
    public String buildBizChildSyncFileName() {
        return Setter.getString("neo4j.bizrelsyncfilename");
    }

    @Override
    public String buildUserChildSyncFileName() {
        return Setter.getString("neo4j.userrelsyncfilename");
    }

结合到neo4j 连接

  1. 根据上面两个步骤, ssm项目启动之后,会自动装载BEAN, 装载之后,只要在具体的程序里面,拿取配置即可(具体的配置获取实现可以由自己实现)
  2. 根据上面的方法使用java拿到xml配置之后,就实现了下面的方式实现neo4j 的数据连接
/**
     * 连接图数据库
     * @return
     */
private void createDriver(Neo4jConfigBuilder builder) {
    if(driver == null){
        try{
            driver =  GraphDatabase.driver(builder.buildUrl(), AuthTokens.basic(builder.buildUsername(), builder.buildPassword()));
        }catch(Exception e){
            driver = null;
            LOGGER.debug("无法建立图数据库连接,请检查图Neo4j 服务是否启动");
            throw new RuntimeException("无法建立图数据库连接,请检查图Neo4j 服务是否启动");
        }
    }
}

具体开发

 /**
     * 执行添加cql
     *
     * @param cql 查询语句
     */
    @Override
    public StatementResult run(Neo4jConfigBuilder builder,String cql) {
        createDriver(builder);
        Session session = driver.session();
        StatementResult run = null;
        try {
            run = session.run(cql);
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            if(session!=null){
                session.close();
            }
        }
        return run;
    }

    @Override
    public StatementResult run(Neo4jConfigBuilder builder,String cql, Object... objects) {
        createDriver(builder);
        Session session = driver.session();
        StatementResult run = null;
        try {
            run = session.run(cql, Values.parameters(objects));
        }catch (Exception e){
            e.printStackTrace();
        } finally {
            if(session!=null){
                session.close();
            }
        }
        return run;
    }

曾经开发的时候查找的一些博客记录:

NEO4J的安装配置及使用总结

neo4j︱neo4j批量导入neo4j-import (五)

neo4j - 查询效率的几种优化思路

Neo4j如何对大量数据(千万节点及以上)进行初始化

关于Neo4j和Cypher批量更新和批量插入优化的5个建议

Neo4j的查询速度为何这么慢?这能商用吗?

Neo4j清空所有数据

Neo4j安装APOC和图算法Neo.ClientError.Procedure.ProcedureRegistrationFailed?

官方网站对于Apoc插件的介绍

neo4j cypher 语言的语法(非常非常重要):

neo4j--Cypher语法练习(START、CREATE、MERGE)

Neo4j中使用Cypher进行大批量节点删除的优化

thinbug 我的很多前端疑难杂症就是靠这网站解决的

查看原文

赞 0 收藏 0 评论 0

lazytimes 发布了文章 · 11月2日

我的Neo4j探索之旅 - 初识Neo4j(一)

前言:

neo4j 这个东西在国内用的很少,目前能百度的资料也是很早之前的几篇了,我针对neo4j 3.5 的版本进行一次学习和记录,以及实际的工作需求我也遇到了,后续会开源一个剔除业务的开源项目,有兴趣的读者可以了解一下图数据库的中间件,还是蛮有意思的。

它是什么?

它的历史

  • 1.0版于2010年2月发布。
  • Neo4j 2.0版于2013年12月发布。
  • Neo4j 3.0版于2016年4月发布。
  • 2016年11月,Neo4j成功获得了由Greenbridge Partners Ltd.牵头的3600万美元D轮融资[[15\]](https://en.wikipedia.org/wiki...
  • 2018年11月,Neo4j成功获得了由One Peak Partners和Morgan Stanley Expansion Capital牵头的8000万美元E轮融资,其他投资者包括Creandum,Eight Roads和Greenbridge Partners参与了此次融资

应用场景

同类产品比较

为什么学习它

  • 公司业务需求,需要使用可视化拓扑图展示数据
  • 标签库使用mysql展示图形结构比较困难,转而使用图数据库解决

为什么要使用neo4j

哪些人不喜欢它

  • neo4j 属于老牌图数据库
  • neo4j 不支持分片,对分布式的系统支持不是很好,推荐单机部署

我要怎么做(按优先级从高到低排序)

  • 看文档:

    • 安装Neo4j desktop

      • 启动,进入localhost: 7474
      • 参考desktop 的快速入门操作案例
      • 进入官网,选择DEVELOP-Document,阅读如下内容:

        • Getting Started 简单的了解Neo4j,地址如下:

        • Cypher manaral 比较重要: CQL语言十分强大,好用
        • Neo4j 1.7 Driver Manual neo4j 驱动以及api使用
      • Java Driver API Docs java API
  • 自己写 Demo

    • 后续会将个人实验内容上传到github
  • 参考别人 Demo

  • 项目场景模拟

    • 让业务去推动技术
    • 明确需求
  • 遇到问题

    • 科学上网到国外使用谷歌进行搜索,目前国内使用较少
    • 查看csdn 博客,有部分问题的解决办法
    • 关于关系型数据库 与 neo4j数据库的数据同步问题
    • Neo4j 与 vis 的使用问题

如何安装neo4j社区版本(免费)(windows - 10)

1. 进入官网,下载软件

https://neo4j.com/graph-algor...

点击右上角:

点击下载:

进入到如下页面

输入对应信息,选择下载

PS: 外网软件的下载真的慢的想死,这里提供一个诀窍

  1. 进入到如下页面,右击蓝色连接
  2. 复制连接地址
  3. 在迅雷里面,新建任务,然后粘贴地址进去
  4. 迅雷会找到资源然后提示你下载
  5. 下载,不出意外飞速下好软件包

(本迅雷为破解版,个人自己使用,不对外开放)

线上的版本使用 版本为3.5.12 ,官方最新版已经有了4.0的版本了

这里我也提供了安装包,自己下的话需要翻墙加迅雷下才有可能拉下来

Windows:

链接:https://pan.baidu.com/s/1yWkI...
提取码:3c21

Linux:

链接:https://pan.baidu.com/s/1ljzS...
提取码:bnrf

2. 安装JDK

这个请自行百度,教程烂大街,不过注意安装 JDK1.8 版本以上,否则是无法使用的

3. 配置Neo4j环境变量

将下好的包解压到对应的位置之后,我们可以配置环境变量

环境变量如下

NEO4J_HOME

D:\zxd\tool\neo4j-community-3.5.12-windows\neo4j-community-3.5.12

4. neo4j 启动

  1. 管理员模式打开CMD, cdD:\\zxd\\tool\\neo4j-community-3.5.12-windows\\neo4j-community-3.5.12\\bin 下面(记得切换盘符)
  2. 请查看下面的常用命令,根据自己的实际情况选择
# 启动服务
neo4j(.bat) start
# 重启服务
neo4j(.bat) restart
# 停止服务
neo4j(.bat) stop
# 控制台模式启动
neo4j(.bat) console
  1. 开启neo4j,看到 类似successful的字样就代表运行成功了
  2. 进入到 http://localhost:7474/browser/
  3. 进入主页面,neo4j安装成功

5. 安装有可能的问题

此部分是针对(4) 有可能失败的情况下进行尝试:

常见问题1

输入如下的命令,安装neo4j 的服务

# 安装neo4j 服务
neo4j install-service
# 卸载neo4j 服务
neo4j uninstall-service

常见问题2:

这种情况可能是你安装服务的时候,neo4j默认已经给你启动了,尝试访问 http://localhost:7474 看下能不能访问,如果可以访问,证明没有出现问题

如果依然没有解决,请尝试 neo4j.bat stop 先关闭服务,或者重新安装一遍neo4j的服务

常见问题3:

下面这个问题是一个比较奇怪的问题,我之前在上线部署的时候遇到过一次

解决办法:imghttps://blog.csdn.net/togethe...

修改bin下面,有一个文件:neo4j.ps1

需要把前缀的 $PSSCRIPT 改为你的安装路径,然后执行neo4j 的命令就不会报错了

6. neo4j 实现外网访问:

conf 里面的 neo4j.conf 中修改:

#dbms.connectors.default_listen_address=0.0.0.0

7. Neo4j 部分配置参数详解:

conf/neo4j.config中有对应的内容:

其他配置项目从网上摘抄部分
修改相应配置如下:

# 修改第22行load csv时l路径,在前面加个#,可从任意路径读取文件
#dbms.directories.import=import

# 修改35行和36行,设置JVM初始堆内存和JVM最大堆内存
# 生产环境给的JVM最大堆内存越大越好,但是要小于机器的物理内存
dbms.memory.heap.initial_size=5g
dbms.memory.heap.max_size=10g

# 修改46行,可以认为这个是缓存,如果机器配置高,这个越大越好
dbms.memory.pagecache.size=10g

# 修改54行,去掉改行的#,可以远程通过ip访问neo4j数据库
dbms.connectors.default_listen_address=0.0.0.0

# 默认 bolt端口是7687,http端口是7474,https关口是7473,不修改下面3项也可以
# 修改71行,去掉#,设置http端口为7687,端口可以自定义,只要不和其他端口冲突就行
#dbms.connector.bolt.listen_address=:7687

# 修改75行,去掉#,设置http端口为7474,端口可以自定义,只要不和其他端口冲突就行
dbms.connector.http.listen_address=:7474

# 修改79行,去掉#,设置http端口为7473,端口可以自定义,只要不和其他端口冲突就行
dbms.connector.https.listen_address=:7473

# 修改227行,去掉#,允许从远程url来load csv
dbms.security.allow_csv_import_from_file_urls=true

# 修改246行,允许使用neo4j-shell,类似于mysql 命令行之类的
dbms.shell.enabled=true

# 修改235行,去掉#,设置连接neo4j-shell的端口,一般都是localhost或者127.0.0.1,这样安全,其他地址的话,一般使用https就行
dbms.shell.host=127.0.0.1

# 修改250行,去掉#,设置neo4j-shell端口,端口可以自定义,只要不和其他端口冲突就行
dbms.shell.port=1337

# 修改254行,设置neo4j可读可写
dbms.read_only=false

8. 修改neo4j可视化界面的超管用户密码]

在控制台输入:server change-password 进行修改

键入原密码及新密码,即可修改

注意冒号

9 . window版本的其他安装方式:

neo4j 在window平台有一个desktop 版本,实现了多实例创建图数据库的应用,有需要可以直接安装,个人直接下载window的Bin包进行单机的部署。

如何安装neo4j社区版本(免费)(linux - CenterOs7)

重复的内容请查看window安装方式,linux 的安装相对更加简单一些。

(1)准备**neo4j-community-3.5.12-unix.tar.gz.gz** ,使用目前最新的版本

Linux:

链接:https://pan.baidu.com/s/1ljzS...
提取码:bnrf

(2)解压放入到linux相应位置

(3)确保当前环境变量存在JDK,版本不能低于JDK1.8

(4)同样由于安全配置的原因,需要进入客户端配置一次用户名和密码,因为linux没有GUI,在neo4j.conf需要开启远程访问:

dbms.connectors.default_listen_address=0.0.0.0

把#拿掉就可以进行远程访问了

(5)请参考window对于用户名和密码进行自定义

(6)如果忘记了GUI页面的用户名和密码,可以使用删除db的方式对于图数据库进行重置

总结:

  1. 介绍了Neo4J的基本理念,已经我为什么要使用到neo4j 这个库
  2. Neo4j在linux上和windows上的安装,注意如果要用到项目上,请注意使用开源的社区版,企业版提供更多的功能以及更好性能,同时官方提供技术支持,商用版本需要授权
  3. 下一篇文章将对neo4j 进行扩展

内容篇幅较长,感谢观看!希望能对读者有所帮助,如果对于博客有任何建议或者意见,欢迎讨论,如果文章内容有误,可以直接私信或者在评论区留言,我会及时答复并且修复

查看原文

赞 0 收藏 0 评论 0

lazytimes 赞了文章 · 10月31日

程序员什么时候就该辞职了?

作为一个技术人员,如果你满足以下条件中的任意一个,你应该去看看更多的机会:

  • 钱少了
  • 技术空间增长太少
  • 没有发展空间
  • 心里委屈了

以及

  • 老板招你的时候,和你谈理想。现在,老板问你说:理想能赚钱吗?
  • 加班太多——都没有时间约会或者女朋友和别人跑了
  • 你的女朋友在北京,而你在上海
  • 这个技术公司已经没有大牛了

程序员该不该裸辞?


程序员千万不要裸辞,对你没有任何好处,你自己看看,信不信由你。

1、不,裸辞 别以为你一整天都能找到更好的。即使你能找到一份更好的工作,中间空闲的精神压力也会让你选择一份糟糕的工作。自由是如此的舒适,它也会让你不想找工作。

2、提前选择三家你想去的公司,做好有针对性的准备。 为什么有三个?因为你可能进不去。为什么只有三个?因为能量有限,你不可能知道这么多公司。

3、选择一组不太可能提前开业的公司。不要一开始就投票给你想去的公司。 为什么?因为熟能生巧,如果你说多次,你的演讲能力和总结能力肯定会提高很多。你也可以了解自己的知识,找出缺失的东西。

4、为面试准备演讲稿,模拟所有可能的问题和答案,写下来,每天阅读,并不断改进。这会让你更舒服。没什么。同级别的人用结构化的描述比拉东西更有说服力。仅此而已。这也是总结自己的好机会。

5、离职前不要骄傲自大,这样全世界都会知道你要离职了。这对团队是有害的,对你没有任何好处。 这是工作场所,不是你的家。专业点,好好聚聚。

6、做好手头工作,不要意气用事,做好交接工作。

7、做好对目标城市企业岗位的社保公积金政策调查,以免在这方面遭受损失,避免因信息不足而跑来跑去。重点城市还可能涉及到居住许可、购房和购车限制等政策。建议在这一领域做出更多努力,以免积累长期资格并失去它们。

8、多给钱,必须决定,坑会更大。更多是让你无偿工作更多,没有地方是不工作就给你更多的钱。 天上不会有馅饼,所以你必须仔细称量。如果市场上有很多家庭愿意给你这么多钱,那么你真的值得。如果只有一个家庭,那么你应该考虑它背后的原因。

9、注意你投资的公司,以免两天内倒闭。 在这种流行病的情况下,每个人都很困难。谁能确定呢?努力寻找大中型企业。摔倒不是那么容易的。

10、对于基础知识必须有一个月左右的关键突破时间,否则找谁推荐你都没用。 不要以为你熟悉你的工作,你可以去面试。面试和工作是两种不同的运作方式。

11、记得向你现在的同事告别。程序员人脉圈一定要维持好;

Java程序员找工作前应该如何准备


Java程序员必备的四点:

  1. Java核心知识
  2. 算法
  3. 面试真题
  4. 优秀的简历

一、《Java精讲》手册


推荐理由:面试官通常会在短短两小时内对面试者的知识结构进行全面了解,面试者在回答问题时如果拖泥带水且不能直击问题的本质,则很难充分表现自己,最终影响面试结果。针对这种情况,本手册在讲解知识点时不拖泥带水,力求精简,详细介绍了Java程序员面试时常被问及的核心知识点,熟读之后可以使面试者在面试时发挥出百分之一百二的实力;

内容主要目录:

由于篇幅原因,本手册里面的小知识点就不一一展开了;有需要完整PDF版的朋友可以点赞后关注公众号【Java斗帝】回复“666”免费获取;

二、左神左程云的算法书籍


推荐理由:好的算法书籍是很多的,但是以Java语言来讲解的算法书籍貌似只有这一本比较不错;(点赞后关注公众号【Java斗帝】免费获取完整PDF版)

具体内容可以找我索要完整PDF版,关于此书的介绍可以简单的看看下面这个图片;

三、大厂面试真题


程序员最好能在空闲的时候看看大厂的面试题,这些面试题的作用有时会超出你的想象:

  1. 学习前辈的面试方法和经验
  2. 检测自己的技术能力,培养职业危机感
  3. 学习更多的技术,锻炼对技术的敏锐度
  4. 了解大厂面试的重难点,为自己的工作提供相应的经验
  5. 了解市场行情,对主流技术留心,思考对自己技术栈的作

所以有一个题目涵盖面大、技术考察丰富的题库是非常有必要的。

四、优秀的简历


简历就是一个人的脸面,多重要应该就不用说了吧;但是写好一份简历并不是很简单;

推荐多研究一下优秀人员的简历模板,还有编写简历时的思路;

上述四份资料【Java精讲+算法+大厂面试+简历模板】只需点赞后关注公众号【Java斗帝】后回复“666”即可获得免费打包下载方式;

最后


再谈谈简历的重要性(摘自知乎)

就是要把你玩过的、觉得很 High 的东西都往上写。毕竟大部分人都是玩游戏过来的,然后你还玩过这么多东西,面试官对你的印象一定特定好。不过,还是那一点,不要造假——写到简历上的东西,都会成为你的呈堂证供。作为一个 Play for Fun 的程序员,谁的大家不是这么玩过来的。

除了这种玩可以为你加分之后,还有:

  1. 211 985高校加分
  2. 硕士学历加分
  3. 大公司实习经验加分
  4. GitHub、博客加分
  5. ACM 等比赛加分
  6. 项目经验加分
  7. 等等

而,等你工作多年后,教育经历就变成无关经历了。这时候加分的内容就变成:

  1. BAT 等大公司加分
  2. NB 的开源项目加分
  3. 与本公司相关的项目经验加分
  4. 行业大牛,自带光环
  5. 技术栈完全匹配加分
  6. GitHub、博客加分
  7. 认识 Phodal 加分 (我的意思:内推)
  8. 等等

这个世界就会从看学校到看公司。所以,如果你毕业的时候遇到这样一个选择:大公司还是创业公司。请考虑上这一点因素,如果这家创业公司倒了,那么你的下家就不好找了。反之,你从大公司要进入小公司,则是轻轻松松地一件事。

记得关注我哦,持续输出程序员相关的一手资料与有趣的故事;

看完三件事❤️


如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。

关注公众号 『 Java斗帝 』,不定期分享原创知识。

同时可以期待后续文章ing🚀

END

see you

查看原文

赞 7 收藏 3 评论 0

lazytimes 收藏了文章 · 10月29日

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

从北京离开之后,@沈兵兵 回到哈尔滨,1平米左右的阳台成了他的工作室,从此他开始了独立产品开发之路。

接下来的一年半时间几乎都是呆在这一平米的地方,后来迫于生活压力,他在哈尔滨找了一份工作,工作期间利用业余时间开发和维护项目,大约去年10月份他选择辞职,粗略算下来这份工作做了一年的时间。

今年因为疫情和家事,全家回到老家县城父母家,厨房就成了工作的地方,晚上成了工作时间。

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

在全职独立开发者的时间里,@沈兵兵陆续打造了十多款产品,其中有三个他最为喜欢,一个关于诗,一个关于酒,还有一个关于远方。

1、精酿啤酒

项目介绍:[精酿笔记]是一款服务家酿啤酒爱好者的酿造辅助工具以及交流社区。
项目背景:2017年,无意中发现原来除了工业啤酒之外,还有精酿啤酒,并且还可以自己在家里酿造。于是阅读相关书籍,购买设备,尝试自己酿造。但是发现国内找不到相关的APP,于是开始自己来写。
面向群体:家酿啤酒爱好者。
建立目的:让家酿啤酒爱好者可以更好的体验酿造过程,同时分享交流酿造经验。

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

2、写诗人

项目介绍:写诗人的出发点是为了创造一个比较存粹的写诗社区。

从用户量上来说,这是一款失败的产品,但是在我心里,它是一款成功的产品,它做到了我心中的那个样子,几年来,在我没有更多精力维护的情况下,用户依然用心的维护它。

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

3、连环画星球

项目介绍:连环画星球是一款为连环画爱好者打造的交流分享社区,用户可以分享自己喜欢的连环画,也可以和其他爱好者共同交流。
立项原因:一是我比较喜欢切小众点的方向,二是连环画是中国的传统艺术形式。三是相对于小说,音频,视频,漫画等,连环画的时间跨度比较长,估计版权问题会好些。

未来修改:一次女儿要听故事,当我用头条音频给她放故事时,受到了启发,我觉得头条发布音频的流程比较适合我的这款产品。于是花了几天时间做了一次大的修改。

目前精酿啤酒项目累计用户8k人,拥有百人以上微信群,百人以上QQ群。但该项目是一个小众领域,目前为止,该项目还未带来多少收益,唯有用户的认可是@沈兵兵坚持做下去的动力,同时该项目也让他后来几款APP有了技术沉淀。

写诗人目前累计注册4k人左右,用户原创诗歌1600余首。采访中@沈兵兵表示:写诗人给我带来更多的是感动,虽然它很小众,但是几年来,用户一直把APP的氛围维护的很好,如若不是生活的压力,我非常想做更多这种有情怀的APP,我也希望以后好起来之后,多做些这种有情怀的产品。

连环画星球是刚刚起步的APP,这款产品让他在如何高效运营产品内容方面,有了新的认识。在这个产品中多次尝试与修改如何高效的维护内容的方案,目前他对方案还算满意。

沈兵兵回忆断断续续个人开发3、4年了,应用也发不了不少,一个月收入千元左右,压力比较大。也有不少人问他要不考虑做点别的,但他很清楚想要什么,喜欢做什么。目前处于家里有事没法工作的状态,他会坚持下去,如果以后还没有起色,他会考虑找份工作,业余来做独立开发者,但是从未想过放弃这条路。

快问快答

1、如何定义“独立开发者”?为什么选择成为一名独立开发者?

如果把互联网比做一个江湖,独立开发者就像独自修炼的人,无门无派。

性格原因占很大一部分,从来接受新鲜事物过程都比较慢,但是一旦喜欢上,就会进入痴迷状态。还有一部分原因是成就感,当一个产品上线,有用户使用,看着后台数据变化,那种感觉是上班没法体会到的。

2、独立开发过程中遇到过哪些困难?最难搞定的是什么?

可以说一直没有走出困境,都在摸索阶段,技术上问题还好,毕竟独立开发,遇到解决不了的问题,可以采用变通的方式解决。最现实的问题是生存问题,产品变现。回想5年前跟女友来到北京,面试了20几家才找到,那时候工资3千多,女友没工作,房租1400,每天只能带饭,想着什么时候工资过万就心满意足了,现如今却发现工资根本不够花,我是一个对钱不那么在乎的人,可不得向现实低头。

还有就是竞品的出现,在精酿笔记上线的几个月后,市场上出现了同类产品,而且各方面都比我优秀,事后我才了解到,那款产品是一家公司在做,当时请了很多酿酒师,可以说有钱有人,而我只有一个人。

3、分享一下你的技术栈?

flutter,react-native,notejs,可能是因为独立开发的原因,其实涉及到的技术还是比较多的,毕竟前端,后台,服务器,数据库,ui等等,但也正是因为这样,所以每项都紧紧达到可以满足需求的水平,并没有深入研究。

思否独立开发者丨@沈兵兵:做独立开发者去追寻真正的诗、酒和远方

4、对开发者们有什么建议

对开发保持热情与初心,这才会坚持下去。正在工作的小伙伴,如果想独立开发,建议先业余时间搞,当时机成熟之后再全力去搞。还有一点就是努力让产品活下去,只有活下去才有希望。

个人建议,不要因为薪资而去选择这个行业,而是因为喜欢,这样未来你的路才不会迷茫。有目标,就要从现在开始做起,无论结果怎样,都要去尝试,才不会遗憾。

5、什么时候开始接触编程的,契机是什么?

大三的时候开始接触编程,在此之前对于编程是没有概念的。当时我的恩师,是一个培训机构的讲师,到我们学校来上培训课,才让我了解到编程,也是因为他当时争取到一个免费培训的名额,而我又有幸获得到了这个名额,从此走上了编程之路。

6、生活中有什么爱好?

  • 酿啤酒,但是回到哈市后因为厨房放不下设备,所以就没有继续酿造了。
  • 打Dota,曾经痴迷的游戏,北漂那会,即使加班很晚回来,也要晚上2把再睡觉,后来因为开始独立编程,没有时间,也就放弃了。

7、如果可以重新选择是否还会选择这个职业

不会选择其他职业,即使在现在我的生活正陷入困境,也有人问我是否考虑转行,我内心给出的答案都是肯定的。我的编程水平可能一般,但是我现在想不到一个能让我即使面对种种困境也如此乐此不疲的职业。

给想成为独立开发者的小伙伴们分享一下自己的心得

1.简单的调研。

那么如何验证自己的想法是否靠谱呢,可以借助很多方式

微信指数小程序,搜索下关键字微信指数,如果指数达到了你的心理预期,则需求存在。

各大应用平台关键字搜索同类产品,如果同类产品非常多,说明大方向没错,那你的产品有自己的唯一不可替代性。

2.初步构思。

构思产品我习惯用思维导图来梳理和完善逻辑,发现问题。

构思产品的初步模型,确定UI,能设计流程图最好,借助墨刀,Axure RP 等,或者采用你喜欢的方式。

3.技术选型。

我之前一年采用的方式一直是服务器用nodejs,客户端用react native,这样的原因是开发语言学习上不需要花太多的时间。客户端使用rn没有使用原生的原因也是因为rn就可以满足我的产品需求而且性能还不错,节省时间成本。但不得不承认我在rn上花了很多时间去填坑,这也是我在下一款产品中改用flutter的原因。

对于APP前端跨平台语言的选择上,需要考虑,性能是否ok,与原生的交互,使用的人数,文档的完善度,社区活跃度,学习成本,综合考虑选择适合自己的就好。

4.开始编写

我的原则是能用三方的尽量用三方,把时间节省出来。

编写过程中,首先保证稳定性,可扩展性。在实现方式上偷懒,但是我不会在功能逻辑上偷懒。

第一个版本上线时间节点很重要,尽量只完成核心功能,这样做好处是缩短开发周期。可以尽快让市场去验证,而且用户反馈可以让你及时作出调整,还能调动你的积极性。至今仍然谨记曾经一位老大说过“小步快跑”。

我觉得这比把一个产品做的自己觉得完美再上线更好。而且开发周期越长,人的耐心会越小,很容易出现半途而废。打磨产品本就是一个漫长且磨人心智的过程,如果能获得用户反馈,无论认可或者批评都会给人很大动力。

现阶段,我基本不会去提前做技术储备,基本都是遇到了问题才会去找网上找实现方式,而且选择的大都是简单初级的方式,这确实比较low。

5.产品完成,部署服务器,申请域名,准备上线。

苹果审核涉及到技术网址,隐私政策,而且谁都希望自己都APP最起码有个下载页,对于提高产品都逼格是有帮助的,所以最好申请个心仪的域名。还有就是小程序需要支持https的域名。服务器的选择,我使用的是阿里云,还是一样选择适合自己的。

关于服务器的运维是需要了解学习一下的,提交平台,APP store的提交流程相比android会麻烦些,应用宝,百度对软著要求比较严格,应用宝第一次审核通过后,后续版本审核会比较快,有时甚至几十分钟过审,一般也在几小时以内,百度,阿里联盟一般在一天以内,小米平台拒绝盒子类APP审核,应用截图有改动必须及时更换。如果能申请某个平台的首发对初始流量获取也帮助很大。android平台众多,其他渠道我了解不多。

6.产品上线,刚刚开始

aso可以帮助更好获得流量,如果你的产品关键字优化到第一页会有可观的流量。切勿重技术轻运营,没有孰轻孰重。尽可能找到好的流量获取入口,有效的引流。

7.有时也许可以改变一种方式

验证一个想法没有必要非得依赖APP,毕竟开发周期比较长,可以尝试使用小程序去验证,周期会缩短,甚至有些想法完全可以通过社群等简单方式去论证,当可行后再开始也许更合适。

总之,我希望的是让产品有它的生命,它会成长,会进化,甚至不需要我的干预,我所做的就是创造出初级的它,而它的成长过程中我需要做的就是服务于它。


独立开发者支持计划-1.png

该内容栏目为「SFIDSP - 思否独立开发者支持计划」。为助力独立开发者营造更好的行业环境, SegmentFault 思否社区作为服务于开发者的技术社区,正式推出「思否独立开发者支持计划」,我们希望借助社区的资源为独立开发者提供相应的个人品牌、独立项目的曝光推介。

有意向的独立开发者或者独立项目负责人,可通过邮箱提供相应的信息(个人简介、独立项目简介、联系方式等),以便提升交流的效率。

联系邮箱:pr@segmentfault.com

独立开发者

查看原文

lazytimes 发布了文章 · 10月29日

一次短信验证码整改实验

一次短信验证码整改实验

前言:

讨论内容部分为当初的一些短信验证码的需求细节讨论

这个短信验证码在并发量非常大的情况下有可能会失效,后续会进行整改升级,保证线程安全

<!-- more -->

需求

短信验证码(要想着怎么把所有的项目都整改起来,不影响原有业务运行) 3天时间,全部替换掉

  • 发送短信

    • 增加【业务类型】
    • 获取短信的时候,增加图片验证码(此处用第三方框架实现)
    • 单独增加短信验证码的 ip访问控制 CheckSendSmsIpVisitHelper ,注意别和 CheckIpVisitHelper 冲突
    • 校验手机号码长度 11 位
    • 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中
    • 可以灵活配置【手机号码+业务】 每天能够获取短信的次数
  • 校验短信

    • 增加【手机号+短信验证码】的匹配
    • 校验成功,清理掉session中存储的信息

讨论内容

  • 验证码就用第三方的。google kaptcha 这个可以试试
  • 这样 jydd apps 都可以用,2-3天可以完全替换掉所有业务的短信。
  • 页面的修改,直接找建哥提供样式。
  • 把所有业务都一并改了,省的隔三差五的出报告要整改
  • 我要一个完全独立的短信验证码模块,虽然不能在按照项目模块来划分,那就完全独立出来包和功能以及依赖
  • 其实可以直接在apps 的 core里面开发,然后那边直接引用调用?jar包的方式放进去就是的。

处理方案:

第三方图形验证码参考

SpringBoot之配置google kaptcha

SpringMvc项目中使用GoogleKaptcha 生成验证码

happy-captcha

图形验证码:

  1. 调整页面,增加图形验证码的模块(不同模块需要的改动点不一样)
也可以先把功能做出来,再让前端根据实际情况去调整样式
  1. 尝试增加 happy-captcha 或者 google-captcha (实际查询资料发现没有进行维护)
  2. 先不考虑美观问题,以实现功能为主要,后续需要改样式在找前端处理
20200903 已实现

短信校验

  1. 60s 根据【手机号码+业务】判断只能发送一次短信,此处将 【手机号码+业务】作为 map 的 key 存储在上下文中
  1. 将短信的配置独立到一个单独的xml 文件中,方便 spirngboot 项目以及 spinrgmvc 管理
  2. 业务模块按照模板的格式配置,不跟项目走
  3. xml 配置读取参考 节假日的xml 配置读取以及作用
  4. 整个部分可以参考节假日的处理方式,迁移比较方便

使用xml 配置手机+业务模块

  1. 校验手机号码长度 11 位
  1. 写到工具类里面,单独增加一个方法,需要的时候在调用的地方加入(方案一)
  2. 直接在调用的地方补充添加(方案二)

直接在短信接口加入即可

  1. 可以灵活配置【手机号码+业务】 每天能够获取短信的次数
  1. 其实就是业务模块单个手机号码的限制次数

PS: 目前的攻击手段可以用虚拟手机号码 + 肉鸡服务器 实现,手机号+业务的限制作用个人理解来看作用不明显

  1. 大致的工具类设置
  • 初始化读取xml配置
  • 加载一些动态配置到属性里面,包括一些校验次数的限制
  • 单例模式
  • 尽量少的使用三方工具包,尽量使用原生java实现
  • 注意jdk 版本的问题,不以jdk1.8 为基准
  • CheckSendSmsIpVisitHelper 可以是对 CheckIpVisitHelper 的扩展
  • happy-captcha 以此作为参考实现 ,google 的图形化验证码比较老了

实现

  1. 目前先尝试 使用一下是否可行, 如果可行在进行处理
  2. 先不考虑样式问题,先以实现功能保证可用并可以迁移为主
  3. 多测试,保证功能稳定,在考虑迁移到apps

大致流程

  1. 输入手机号码
  2. 在点击发送短信按钮之前,弹出输入图形验证码
  3. 输入正确的图形验证码,发送短信,图形验证取消,回到输入手机验证码界面
  4. 如果输入推行验证码不对,一直重复步骤2

问题:

1. 绕过图形验证码的接口,直接访问短信接口进行攻击,如何避免?

解决方案:

  1. 在 【手机号+业务】中增加一个图形验证码的key, 在校验之前,先校验用户当前提交的手机号和图形验证码是否匹配

    1. 匹配:清空用户当前的图形验证码,需要重新请求图形验证码接口才能进行下一次请求
    2. 超时:图形验证码有效时间为60秒,超过60秒需要重新请求图形验证码,重新请求短信接口
    3. 不匹配,返回错误信息
  2. 设置校验开关,如果需要在短信接口加入图形验证码的校验,则在发送短信之前,需要当前的图形验证码是否吻合(如果没有图形验证码说明没有经过图形验证这一步骤,直接返回失败)

    1. 图形验证码校验关闭,则不会涉及用户图形验证码和手机号的匹配(考虑临时需要关闭这种校验的情况)
    2. 开启,则会进行上面所说的匹配操作
  3. 图形验证码设置为通过校验之后失效,下次请求需要携带新的图形验证码,才能请求通过短信接口

2. 增加【手机号-业务】的配置校验

解决方案:

  1. 短信模块需要在js请求增加模块参数,如果没有模块参数,视为非法请求
  2. 请求带入 手机号-业务-key ,存储当前手机号对应业务的请求次数
  3. 增加判断

    1. 如果请求次数在当天内超过xml配置次数,将不再允许改手机号对应该接口进行请求,不再发送短信
    2. 但是如果超过了一天之后再次请求,需要将请求次数 重置为1,也可以重新发送短信接口请求
  4. 在需要的地方调用工具包即可

实现过程:

成果:

控制器调用:

 // 开启之后,才做进一步校验
if(PHONE_MODULE_CHECK_ENABLE){
    // 添加 【手机+业务模块】校验 以及 【60秒重复调用校验】
    boolean checkRequest = CheckSendMailHelper.checkContextMap(result, request, phone);
    // 校验不通过的处理办法,可以自定
    if (!checkRequest) {
        return result;
    }
}

//限制用户ip访问短信机获取验证码次数,默认10次
if (IP_CHECK_ENABLE && !CheckIpVisitHelper.check(request)) {
    // 校验不通过的处理办法,可以自定
    result.put("result", SmsRequestStatusEnum.RESULT_STATUS_6.getCode());
    result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_6.getName());
    return result;
}
PHONE_MODULE_CHECK_ENABLE:短信-业务模块校验开关

IP_CHECK_ENABLE:限制短信每天的获取次数,也是手机号+业务模块

短信相关的枚举常量:

public enum SmsRequestStatusEnum {

    /**
     * 返回状态码 表示发送正常
     */
    RESULT_STATUS_1(1, "返回状态码 表示发送正常"),
    /**
     * 60s内只能获取不能重复获取验证码
     */
    RESULT_STATUS_2(2, "60s内只能获取不能重复获取验证码"),
    /**
     * 手机号码长度不正确
     */
    RESULT_STATUS_3(3, "手机号码长度不正确"),
//    /**
//     * 用户session已失效
//     */
//    RESULT_STATUS_4(4, "用户session已失效"),
    /**
     * 缺少必要的参数:手机号!
     */
    RESULT_STATUS_4(4, "缺少必要的参数:手机号!"),
    /**
     * 手机号码长度不正确
     */
    RESULT_STATUS_5(5, "手机号码长度不正确"),
    /**
     * 同一个ip请求短信机次数过于频繁
     */
    RESULT_STATUS_6(6, "同一个ip请求短信机次数过于频繁!"),

    /**
     * 60秒内不允许重复请求短信接口
     */
    RESULT_STATUS_7(7, "60秒内不允许重复请求短信接口!"),
    /**
     * 缺少必要的请求参数:短信业务模块名称:phoneModule !
     */
    RESULT_STATUS_9(9, "缺少必要的请求参数:短信业务模块名称:phoneModule !"),
    /**
     * 当前手机号请求超出限制,请等待24小时之后重新请求短信接口
     */
    RESULT_STATUS_10(10, "当前手机号请求次数超出限制,请等待24小时之后重新请求短信接口"),
    /**
     * 图形验证码已失效,请重新请求短信接口!
     */
    RESULT_STATUS_8(8, "图形验证码已失效,请重新请求短信接口!");

    private int code;
    private String name;

    SmsRequestStatusEnum(int code, String name) {
        this.code = code;
        this.name = name;
    }

    public int getCode() {
        return code;
    }

    public String getName() {
        return name;
    }

    public static String getName(int code) {
        for (SmsRequestStatusEnum item : SmsRequestStatusEnum.values()) {
            if (item.getCode() == code) {
                return item.getName();
            }
        }
        return "";
    }
}

IP检测工具类:

/**
 * 检测ip访问辅助类,
 * 主要处理某个时间段类,
 * ip访问次数,以及设置封禁时间、
 * 解封等操作,
 * 用于防止频繁调用短信机攻击等
 * <p>
 * <p>
 * 重写原理:
 * 1. 使用LRUMap key 存储 IP号码,value 存储 访问次数以及时间(使用map)
 * 2. 使用servletContext 存储 LRUMap,LRUMap 存储 的 key 为 IP号码-业务模块 VALUE 为 map
 * 3. LRUMap 对应的 key IP号码+业务。 value 绑定了访问次数和时间
 * 4. 如果没有配置模块,校验将会永久失败,IP的模块和短信的模块使用同一块配置
 * 5. ServletContext 生命周期和web的生命周期相同
 *
 * 2020/09/08 重写工具类,
 * 1. 不在暴露 map。
 * 2. 使用servletContext 保存 Ip 的 map。Map<String,Object> 形式
 * 3. 如果超过IP限制时间,自动进行解锁
 *
 * @author xd
 */
public class CheckIpVisitHelper {

    /**
     * 日志使用 短信的key
     */
    private static final Logger logger = LoggerFactory.getLogger("phoneCode");

    /**
     * 手机访问限制初始化的值
     */
    private static final int PHONE_REQUEST_INIT_SIZE = 1;

    /**
     * 封禁的时间(单位毫秒)
     */
    private static final int FORBIDEN_TIME = 60 * 1000 * 60;
    /**
     * 超过访问时间重新计时(单位毫秒)
     */
    private static final int MININTEVAL = 60 * 1000 * 60;

    /**
     * LRU Map 初始化大小
     */
    private static final int LRU_MAP_INIT_SIZE = 100000;

    /**
     * IP 在指定时间内的限制次数
     */
    private static final int IP_MAX_VISIST_TIME = Setter.getInt("sms.ip-size");

    /**
     * ip检测使用的 Map key
     */
    private static final String IP_CHECK_MAP = "IP_CHECK_MAP";

    /**
     * 请求次数
     */
    private static final String VISIT_COUNT_KEY = "visit_count";

    /**
     * 最后的请求时间
     */
    private static final String VISIT_TIME_KEY = "visit_time";

    /**
     * IP号码-业务模块名称的格式
     */
    private static final String IP_MOUDULE_FORMAT = "%s-%s";

    /**
     * ip检查工具,将map 放入 ServletContext
     * 1. 检测基于 ServletContext
     * 2. 请附带 phoneModule: 否则校验永远为false
     * map 当中:
     * key: IP号码-业务
     * value:
     * map -> {
     *  key: 请求次数:value: int
     *  key:请求的时间:value:date
     * }
     *
     * @param request request请求域
     * @return 如果校验没有超过限制 返回 true ,否则返回false
     */
    public static boolean check(HttpServletRequest request) {
        String remoteIp = RequestHelper.getRemoteIp(request);
        ServletContext servletContext = request.getServletContext();
        LRUMap attribute = (LRUMap) servletContext.getAttribute(IP_CHECK_MAP);
        if (Objects.isNull(attribute)) {
            attribute = new LRUMap(LRU_MAP_INIT_SIZE);
            servletContext.setAttribute(IP_CHECK_MAP, attribute);
        }
        Date now = new Date();
        // 根据 IP + 业务模块进行绑定
        // 获取请求的模块名称 同时检查是否有配置模块
        String phoneMouduleFlag = CheckSendMailHelper.checkExistsAndGetModule(request);
        if (phoneMouduleFlag == null) {
            return false;
        }
        // IP号码 -业务名称
        String modulePhone = String.format(IP_MOUDULE_FORMAT, remoteIp, phoneMouduleFlag);
        // 获取ip对应的的当前请求次数和请求时间
        Map<String, Object> ipMap = (Map<String, Object>) attribute.get(modulePhone);
        // 如果当前ip没有访问过
        if (MapUtils.isEmpty(ipMap)) {
            ipMap = new HashMap<>();
            ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
            ipMap.put(VISIT_TIME_KEY, now);
            attribute.putIfAbsent(modulePhone, ipMap);
            return true;
        }
        int visitCount = (int) ipMap.get(VISIT_COUNT_KEY);
        Date visitDate = (Date) ipMap.get(VISIT_TIME_KEY);
        // 如果长时间没有访问,重新计算
        if (now.getTime() - visitDate.getTime() > MININTEVAL) {
            ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
            ipMap.put(VISIT_TIME_KEY, now);
            return true;
        }
        // 如果访问的次数超过了限制的次数
        if (visitCount > IP_MAX_VISIST_TIME) {
            // 如果已经到达限制的次数,但是访问时间超过了限制的时间,重新计时,重新计算请求次数
            if (now.getTime() - visitDate.getTime() > FORBIDEN_TIME) {
                ipMap.put(VISIT_COUNT_KEY, PHONE_REQUEST_INIT_SIZE);
                ipMap.put(VISIT_TIME_KEY, now);
                return true;
            }
            logger.info("当前IP: {} 请求次数超过限制", remoteIp);
            return false;
        } else {
            // IP访问次数 + 1
            visitCount++;
            // 更新访问次数
            ipMap.put(VISIT_COUNT_KEY, visitCount);
            // 更新访问时间
            ipMap.put(VISIT_TIME_KEY, now);
        }
        return true;
    }




}
  1. 使用的是servlet-context 全局变量作为存储,依赖web的服务器空间大小,当短信号码过量会造成服务器可访问内存不够,可以考虑用redis 等中间件去存储
  2. LRUMap:使用最少使用内容作为缓存的设计,存储业务需要判断的手机号等
  3. 静态方法意味着会出现并发的问题,整个工具类是线程不安全的。

短信发送校验工具类:

/**
 * 短信发送校验工具类
 * map 存储的 key 为手机号码-业务
 * value  为 发送对象等其他信息
 * 包含
 * 1. 图形验证码(不开放不做校验)
 * 2. 图形验证码有效时间
 * 3. 【手机号-业务】 key-name 的配置
 * 4. 【手机号-业务-锁定时间】 key-date
 * 
 *
 * @Author lazytimes
 * @Date 2020/09/02 10:21
 **/
public class CheckSendMailHelper {

    /**
     * 短信验证码配置
     */
    private static final Logger logger = LoggerFactory.getLogger("phoneCode");

    /**
     * 60 秒内不允许重复请求
     */
    private static final int PHONE_REQUEST_TIME = 60 * 1000;

    /**
     * 60 秒 内 图形验证码有效
     */
    private static final int CAPTCHA_REQUEST_TIME = 60 * 1000;

    /**
     * 用户模块手机号的限制时间 24 小时
     */
    private static final int PHONE_REQUEST_WAIT_TIME = 60 * 1000 * 24 * 60;

    /**
     * 手机访问限制初始化的值
     */
    private static final int PHONE_REQUEST_INIT_SIZE = 1;

    /**
     * 请求上下文的map key
     */
    private static final String CONTEXT_MAP = "CONTEXT_MAP";

    /**
     * 手机号-业务模块名称的格式
     */
    private static final String PHONE_MOUDULE_FORMAT = "%s-%s";


    /**
     * 手机号-业务模块-请求key 的格式标注用户当前模块的请求 定时器
     */
    private static final String PHONE_MOUDULE_TIMER_FORMAT = "%s-%s-timer";

    /**
     * 短信验证码模块的通用格式
     */
    private static final String SMS_MODULE_TEMPLATE = "sms.modules.%s";

    /**
     * 手机号-业务-图形验证码 模块名称的格式
     */
    private static final String CAPTCHA_MOUDULE_FORMAT = "%s-%s-captcha";

    /**
     * 手机号-业务模块-图形验证码-请求key 的格式标注用户当前模块的请求 图形验证码 每个手机号对应业务一份
     */
    private static final String CAPTCHA_MOUDULE_TIMER_FORMAT = "%s-%s-captcha-timer";

    /**
     * 业务模块名称参数Key
     */
    private static final String PHONE_MOUDULE_FLAG = "phoneModule";

    /**
     * 图形验证码key
     */
    private static final String CAPCHACODE = "capchaCode";

//    /**
//     * 最后发送时间key
//     */
//    private static final String LAST_SEND_TIME = "lastSendTime";

    /**
     * 图形验证码开关
     */
    private static final boolean CAPTCHA_ENABLE = Setter.getBoolean("captcha.enable");


    /**
     * 为当前的用户手机号码绑定 图形验证码
     * 图形验证码用于短信接口请求使用,超过一定时间,图形验证码失效
     * 【手机号-业务-图形验证码】:key
     * 【手机号-业务-图形验证码-超时时间】:key
     *
     * @param phoneCode 手机号
     * @param code      图形验证码
     * @param request   请求
     */
    public static void addCapcha(String phoneCode, String code, HttpServletRequest request) {
        if (!CAPTCHA_ENABLE) {
            logger.info("请开启图形验证码校验之后,再配合本工具类方法使用!");
            return;
        }
        ServletContext servletContext = request.getServletContext();
        Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
        Date now = new Date();
        // 获取请求的模块名称 同时检查是否有配置模块
        String phoneMouduleFlag = checkExistsAndGetModule(request);
        if (StringUtils.isBlank(phoneMouduleFlag)) {
            return;
        }
        // 手机号 -业务名称
        String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
        // 手机号- 业务名称 - 图形验证码
        String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
        // 手机号 - 业务名称 -图形验证码 - 定时
        String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
        if (!attribute.containsKey(modulePhone)) {
            HashMap<String, Object> stringObjectHashMap = new HashMap<>();
            stringObjectHashMap.put(capchaModule, code);
            // 图片的有效期
            stringObjectHashMap.put(capchaModuleTimer, now);
            attribute.put(modulePhone, stringObjectHashMap);
        } else {
            Map<String, Object> stringObjectMap = attribute.get(modulePhone);
            // 更新验证码以及有效期
            stringObjectMap.put(capchaModule, code);
            // 图片的有效期
            stringObjectMap.put(capchaModuleTimer, now);
        }
    }

    /**
     * 手机号限制发送处理
     * 1. 增加对于用户请求短信接口的限制,60秒访问一次
     * 2. 增加图形验证码和用户的手机号绑定匹配
     * 1. 图形校验可以灵活开放和关闭
     * 3. 【手机号-业务】的key配置,短信接口当中需要对于用户的请求做限制
     *
     * @param result    封装了返回的状态和信息的 result
     * @param request   请求request
     * @param phoneCode 手机号码
     * @return
     */
    public static boolean checkContextMap(Map<String, Object> result, HttpServletRequest request, String phoneCode) {
        // 获取当前模块配置Map集合
        ServletContext servletContext = request.getServletContext();
        Map<String, Map<String, Object>> attribute = initServletContextMap(servletContext);
        Date now = new Date();
        // 获取请求的模块名称
        String phoneMouduleFlag = checkExistsAndGetModule(request);
        if (phoneMouduleFlag == null) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_9.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_9.getName());
            return false;
        }
        //  当前短信业务模块【手机号-业务】
        String modulePhone = String.format(PHONE_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
        // 当前模块【手机号-业务-请求限制时间】
        String modulePhoneTimer = String.format(PHONE_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
        // 当前模块每个用户每天最多请求次数
        int moduleCount = Setter.getInt(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
        if (!attribute.containsKey(modulePhone)) {
            // 需要自行初始化
            HashMap<String, Object> stringObjectHashMap = new HashMap<>();
            // 初始化短信接口调用次数
            stringObjectHashMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
            // 初始化短信接口调用时间
            stringObjectHashMap.put(modulePhoneTimer, now);
            attribute.put(modulePhone, stringObjectHashMap);
            return true;
        } else {
            Map<String, Object> objectMap = attribute.get(modulePhone);
            // 开启图形验证码校验才做处理
            if (CAPTCHA_ENABLE) {
                if (!checkCatpchaCode(result, request, phoneCode, now, phoneMouduleFlag, objectMap)) {
                    return true;
                }
            }
            // 获取当前【手机号+业务】的对应 访问次数,以及最后的访问时间
            Object count = objectMap.get(modulePhone);
            Object timer = objectMap.get(modulePhoneTimer);
            // 初始化
            if (Objects.isNull(count) || Objects.isNull(timer)) {
                objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                objectMap.put(modulePhoneTimer, now);
                return true;
            }
            Integer integer = Integer.valueOf(objectMap.get(modulePhone).toString());
            Date time = (Date) timer;
            // 检查当前短信+业务是否在60秒内访问
            if(!checkLastGetTime(result, now, time)){
                return false;
            }
            //如果长时间未访问,重置
            if ((now.getTime() - time.getTime()) > PHONE_REQUEST_WAIT_TIME) {
                //  刷新时间
                objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                objectMap.put(modulePhoneTimer, now);
                return true;
            }
            //   当前模块超过了请求限制
            if (integer > moduleCount) {
                // 超过了请求时间限制,解封
                if (now.getTime() - time.getTime() > PHONE_REQUEST_WAIT_TIME) {
                    //  刷新时间
                    objectMap.put(modulePhone, PHONE_REQUEST_INIT_SIZE);
                    objectMap.put(modulePhoneTimer, now);
                    return true;
                }
                result.put("result", SmsRequestStatusEnum.RESULT_STATUS_10.getCode());
                result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_10.getName());
                return false;
            }
            // 模块请求次数 + 1
            objectMap.put(modulePhone, integer + PHONE_REQUEST_INIT_SIZE);
            // 刷新时间
            objectMap.put(modulePhoneTimer, now);
        }
        return true;
    }

    /**
     * 校验图形验证码
     *
     * @param result           返回处理结果
     * @param request          请求
     * @param phoneCode        手机号
     * @param now              当前时间
     * @param phoneMouduleFlag 手机号 - 业务模块 标识
     * @param objectMap        servletContext 对象
     * @return
     */
    private static boolean checkCatpchaCode(Map<String, Object> result, HttpServletRequest request, String phoneCode, Date now, String phoneMouduleFlag, Map<String, Object> objectMap) {
        // 手机号- 业务名称 - 图形验证码
        String capchaModule = String.format(CAPTCHA_MOUDULE_FORMAT, phoneCode, phoneMouduleFlag);
        // 手机号 - 业务名称 -图形验证码 - 定时
        String capchaModuleTimer = String.format(CAPTCHA_MOUDULE_TIMER_FORMAT, phoneCode, phoneMouduleFlag);
        // 图形验证码超过60秒失效
        Date captchaCodeValidPeriod = (Date) objectMap.get(capchaModuleTimer);
        // 获取请求参数的验证码
        String requestCaptchaCode = RequestHelper.getString(CAPCHACODE, request);
        // 拿到map中的图形验证码
        Object requestCode = objectMap.get(capchaModule);
        // 是否存在图形验证码的参数,同时比对是否和请求参数一致
        if (StringUtils.isBlank(requestCaptchaCode) || Objects.isNull(requestCode)) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
            return false;
        }
        // 如果超时或者图形验证码不匹配,需要重新请求图形验证码
        if (!Objects.equals(requestCaptchaCode, requestCode.toString()) || (now.getTime() - captchaCodeValidPeriod.getTime() > (CAPTCHA_REQUEST_TIME))) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_8.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_8.getName());
            return false;
        } else {
            // 清空用户的图形验证码
            objectMap.put(capchaModule, null);
        }
        return true;
    }

    /**
     * 检查最后的访问时间是否在指定时间内容
     *
     * @param result    返回对象结果
     * @param now       当前时间
     * @return
     */
    private static boolean checkLastGetTime(Map<String, Object> result, Date now, Date lastSend) {
        // 60 秒内不允许再次发送
        if ((now.getTime() - lastSend.getTime()) <= PHONE_REQUEST_TIME) {
            result.put("result", SmsRequestStatusEnum.RESULT_STATUS_7.getCode());
            result.put("msg", SmsRequestStatusEnum.RESULT_STATUS_7.getName());
            return false;
        }
        return true;
    }

    /**
     * 初始化全局上下文的Map容器
     *
     * @param servletContext 上下文
     * @return 初始化之后的map参数
     */
    private static Map<String, Map<String, Object>> initServletContextMap(ServletContext servletContext) {
        Map<String, Map<String, Object>> attribute = (Map<String, Map<String, Object>>) servletContext.getAttribute(CONTEXT_MAP);
        if (Objects.isNull(attribute)) {
            attribute = new HashMap<>();
            servletContext.setAttribute(CONTEXT_MAP, attribute);
        }
        return attribute;
    }


    /**
     * 检查请求参数中是否存在业务模块配置
     *
     * @param request 请求request
     * @return
     */
    static String checkExistsAndGetModule(HttpServletRequest request) {
        String phoneMouduleFlag = RequestHelper.getString(PHONE_MOUDULE_FLAG, request);
        String moduleNo = Setter.getString(String.format(SMS_MODULE_TEMPLATE, phoneMouduleFlag));
        if (StringUtils.isBlank(moduleNo)) {
            logger.info("未找到对应的短信模块,请在xml配置短信模块名称,并在请求参数中加入 phoneModule: 对应模块名称之后再进行请求");
            return null;
        }
        return phoneMouduleFlag;
    }

}
  1. 使用手机号-业务模块。先获取是否存在对应的模块,然后进行校验
  2. 图形验证码的方法需要开启推行验证码的情况下,配合使用
  3. HashMap的线程是不安全的,可以考虑使用ConcurrentHashMap

图形验证码的配置:

<!-- =================================================================== -->
    <!-- 核心:图形验证码的通用配置 -->
    <!-- =================================================================== -->
    <captcha description="图形验证码的通用配置">
        <enable description="是否开放图形验证码" value="false" />
        <length description="设置字符长度" value="5" />
        <!-- 验证码图片的宽度 默认 160 -->
        <width description="设置动画宽度" value="160" />
        <!-- 验证码图片的高度 默认 50 -->
        <height description="设置动画宽度" value="50" />
    </captcha>

短信验证码的配置:

<!-- =================================================================== -->
    <!--系统发送短信配置 -->
    <!-- =================================================================== -->
    <sms description="webService短信机服务配置">
        <isopen description="是否开启短信发送" value="true"/>
        <!-- 模块配置: 需要 name 模块名称,用于短信校验 和 value 表示每天最多的请求次数 -->
        <modules>
            <!-- 注册模块 -->
            <registered description="注册模块" value="5"/>
            <!-- 信箱请求短信验证码 -->
            <mailbox description="信箱模块" value="10"/>
        </modules>
        <ip-size description="ip检测的限制次数" value="10"/>
        <phoneMoudleCheck-enable description="手机号-业务模块校验是否开启" value="true"/>
        <ip-enable description="IP检测开关" value="true"/>
    </sms>

总结:

  1. 工具类基于配置进行开关配置
  2. 按照手机号+业务模块,划分同一手机号在不同的业务模块进行校验拦截
  3. 图形验证码可以配合短信接口使用,但是目前来看耦合还是有点严重

结语:

本人学艺不精,代码写的比较烂,这篇文章算是给自己留坑以后填。

如果看文章费劲头,专门另写一篇说说独立使用。

小小工具类,仅供参考

查看原文

赞 1 收藏 1 评论 0

认证与成就

  • 获得 4 次点赞
  • 获得 2 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 2 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 5月10日
个人主页被 575 人浏览