暂时未有相关云产品技术能力~
在实际的项目中,服务高可用非常重要,如,当Redis作为缓存服务使用时, 缓解数据库的压力,提高数据的访问速度,提高网站的性能 ,但如果使用Redis 是单机模式运行 ,只要一个服务器宕机就不可以提供服务,这样会可能造成服务效率低下,甚至出现其相对应的服务应用不可用。因此为了实现高可用,Redis 提供了哪些高可用方案?Redis主从复制Redis持久化哨兵集群...Redis基于一个Master主节点多Slave从节点的模式和Redis持久化机制,将一份数据保持在多个实例中实现增加副本冗余量,又使用哨兵机制实现主备切换, 在master故障时,自动检测,将某个slave切换为master,最终实现Redis高可用 。Redis主从复制Redis主从复制,主从库模式一个Master主节点多Slave从节点的模式,将一份数据保存在多Slave个实例中,增加副本冗余量,当某些出现宕机后,Redis服务还可以使用。但是这会存在数据不一致问题,那redis的副本集是如何数据一致性?Redis为了保证数据副本的一致,主从库之间采用读写分离的方式:读操作:主库、从库都可以执行处理;写操作:先在主库执行,再由主库将写操作同步给从库。使用读写分离方式的好处,可以避免当主从库都可以处理写操作时,主从库处理写操作加锁等一系列巨额的开销。采用读写分离方式,写操作只会在主库中进行后同步到从库中,那主从库是如何同步数据的呢?主从库是同步数据方式有两种:全量同步:通常是主从服务器刚刚连接的时候,会先进行全量同步增量同步 :一般在全同步结束后,进行增量同步,比如主从库间网络断,再进行数据同步。全量同步主从库间第一次全量同步,具体分成三个阶段:当一个从库启动时,从库给主库发送 psync 命令进行数据同步(psync 命令包含:主库的 runID 和复制进度 offset 两个参数),当主库接收到psync 命令后将会保存RDB 文件并发送给从库,发送期间会使用缓存区(replication buffer)记录后续的所有写操作 ,从库收到数据后,会先清空当前数据库,然后加载从主库获取的RDB 文件,当主库完成 RDB 文件发送后,也会把将保存发送RDB文件期间写操作的replication buffer发给从库,从库再重新执行这些操作。这样一来,主从库就实现同步了。另外,为了分担主库生成 RDB 文件和传输 RDB 文件压力,提高效率,可以使用 “主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。增量同步增量同步,基于环形缓冲区repl_backlog_buffer缓存区实现。在环形缓冲区,主库会记录自己写到的位置 master_repl_offset ,从库则会记录自己已经读到的位置slave_repl_offset, 主库并通过master_repl_offset 和 slave_repl_offset的差值的数据同步到从库。主从库间网络断了, 主从库会采用增量复制的方式继续同步,主库会把断连期间收到的写操作命令,写入 replication buffer,同时也会把这些操作命令也写入 repl_backlog_buffer 这个缓冲区,然后主库并通过master_repl_offset 和 slave_repl_offset的差值数据同步到从库。因为repl_backlog_buffer 是一个环形缓冲区,当在缓冲区写满后,主库会继续写入,此时,会出现什么情况呢?覆盖掉之前写入的操作。如果从库的读取速度比较慢,就有可能导致从库还未读取的操作被主库新写的操作覆盖了,这会导致主从库间的数据不一致。因此需要关注 repl_backlog_size参数,调整合适的缓冲空间大小,避免数据覆盖,主从数据不一致。主从复制,除了会出现数据不一致外,甚至可能出现主库宕机的情况,Redis会有主从自主切换机制,那如何实现的呢?Redis哨兵机制当主库挂了,redis写操作和数据同步无法进行,为了避免这样情况,可以在主库挂了后重新在从库中选举出一个新主库,并通知到客户端,redis提供了 哨兵机制,哨兵为运行在特殊模式下的 Redis 进程。Redis会有主从自主切换机制,那如何实现的呢?哨兵机制是实现主从库自动切换的关键机制,其主要分为三个阶段:监控:哨兵进程会周期性地给所有的主从库发送 PING 命令,检测它们是否仍然在线运行。选主(选择主库):主库挂了以后,哨兵基于一定规则评分选选举出一个从库实例新的主库 。通知 : 哨兵会将新主库的信息发送给其他从库,让它们和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的信息广播通知给客户端,让它们把请求操作发到新主库上。其中,在监控中如何判断主库是否处于下线状态?哨兵对主库的下线判断分为:主观下线:哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接情况,用来判断实例的状态, 如果单哨兵发现主库或从库对 PING 命令的响应超时了,那么,哨兵就会先把它标记为“主观下线”客观下线:在哨兵集群中,基于少数服从多数,多数实例都判定主库已“主观下线”,则认为主库“客观下线”。为什么会有这两种"主观下线"和“客观下线”的下线状态呢?由于单机哨兵很容易产生误判,误判后主从切换会产生一系列的额外开销,为了减少误判,避免这些不必要的开销,采用哨兵集群,引入多个哨兵实例一起来判断,就可以避免单个哨兵因为自身网络状况不好,而误判主库下线的情况,基于少数服从多数原则, 当有 N 个哨兵实例时,最好要有 N/2 + 1 个实例判断主库为“主观下线”,才能最终判定主库为“客观下线” (可以自定义设置阙值)。那么哨兵之间是如何互相通信的呢?哨兵集群中哨兵实例之间可以相互发现,基于 Redis 提供的发布 / 订阅机制(pub/sub 机制),哨兵可以在主库中发布/订阅消息,在主库上有一个名为“\__sentinel__:hello”的频道,不同哨兵就是通过它来相互发现,实现互相通信的,而且只有订阅了同一个频道的应用,才能通过发布的消息进行信息交换。哨兵 1连接相关信息(IP端口)发布到“\__sentinel__:hello”频道上,哨兵 2 和 3 订阅了该频道。哨兵 2 和 3 就可以从这个频道直接获取哨兵 1连接信息,以这样的方式哨兵集群就形成了,实现各个哨兵互相通信。哨兵集群中各个实现通信后,就可以判定主库是否已客观下线。在已判定主库已下线后,又如何选举出新的主库?新主库选举按照一定条件筛选出的符合条件的从库,并按照一定规则对其进行打分,最高分者为新主库。通常一定条件包括:从库的当前在线状态,判断它之前的网络连接状态,通过down-after-milliseconds * num(断开连接次数),当断开连接次数超过阈值,不适合为新主库。一定规则包括:从库优先级 , 通过slave-priority 配置项,给不同的从库设置不同优先级,优先级最高的从库得分高从库复制进度,和旧主库同步程度最接近的从库得分高,通过repl_backlog_buffer缓冲区记录主库 master_repl_offset 和从库slave_repl_offset 相差最小高分从库 ID 号 , ID 号小的从库得分高。全都都基于在只有在一定规则中的某一轮评出最高分从库就选举结束,哨兵发起主从切换。leader哨兵选举完新的主库后,不能每个哨兵都发起主从切换,需要选举成leader哨兵,那如何选举leader哨兵执行主从切换?选举leader哨兵,也是基于少数服从多数原则"投票仲裁"选举出来,当任何一个从库判定主库“主观下线”后,发送命令 s-master-down-by-addr命令发送想要成为Leader的信号,其他哨兵根据与主机连接情况作出相对的响应,赞成票Y,反对票N,而且如果有多个哨兵发起请求,每个哨兵的赞成票只能投给其中一个,其他只能为反对票。想要成为Leader 的哨兵,要满足两个条件:第一,获得半数以上的赞成票;第二,获得的票数同时还需要大于等于哨兵配置文件中的quorum值。选举完leader哨兵并新主库切换完毕之后,那么leader哨兵怎么通知客户端?还是基于哨兵自身的 pub/sub 功能,实现了客户端和哨兵之间的事件通知,客户端订阅哨兵自身消息频道 ,而且哨兵提供的消息订阅频道有很多,不同频道包含了:事件相关频道主库下线事件+sdown(实例进入“主观下线”状态)-sdown(实例退出“主观下线”状态)+odown(实例进入“客观下线”状态)-odown(实例退出“客观下线”状态)新主库切换+ switch-master(主库地址发生变化)其中,当客户端从哨兵订阅消息主从库切换,当主库切换后,端户端就会接收到新主库的连接信息:switch-master <master name> <oldip> <oldport> <newip> <newport> 复制代码在这样的方式哨兵就可以通知客户端切换了新库。基于上述的机制和原理Redis实现了高可用,但也会带了一些潜在的风险,比如数据缺失。数据问题Redis实现高可用,但实现期间可能产出一些风险:主备切换的过程, 异步复制导致的数据丢失脑裂导致的数据丢失主备切换的过程,异步复制导致数据不一致数据丢失-主从异步复制因为master 将数据复制给slave是异步实现的,在复制过程中,这可能存在master有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了。总结:主库的数据还没有同步到从库,结果主库发生了故障,未同步的数据就丢失了。数据丢失-脑裂何为脑裂?当一个集群中的 master 恰好网络故障,导致与 sentinal 通信不上了,sentinal会认为master下线,且sentinal选举出一个slave 作为新的 master,此时就存在两个 master了。此时,可能存在client还没来得及切换到新的master,还继续写向旧master的数据,当master再次恢复的时候,会被作为一个slave挂到新的master 上去,自己的数据将会清空,重新从新的master 复制数据,这样就会导致数据缺失。总结:主库的数据还没有同步到从库,结果主库发生了故障,等从库升级为主库后,未同步的数据就丢失了。数据丢失解决方案数据丢失可以通过合理地配置参数 min-slaves-to-write 和 min-slaves-max-lag 解决,比如min-slaves-to-write 1min-slaves-max-lag 10如上两个配置:要求至少有 1 个 slave,数据复制和同步的延迟不能超过 10 秒,如果超过 1 个 slave,数据复制和同步的延迟都超过了 10 秒钟,那么这个时候,master 就不会再接收任何请求了。数据不一致在主从异步复制过程,当从库因为网络延迟或执行复杂度高命令阻塞导致滞后执行同步命令,这样就会导致数据不一致解决方案: 可以开发一个外部程序来监控主从库间的复制进度(master_repl_offset 和 slave_repl_offset ),通过监控 master_repl_offset 与slave_repl_offset差值得知复制进度,当复制进度不符合预期设置的Client不再从该从库读取数据。总结Redis使用主从复制、持久化、哨兵机制等实现高可用,需要理解其实现过程,也要明白其带了风险以及解决方案,才能在实际项目更好优化,提升系统的可靠性、稳定性。谢谢各位点赞,没点赞的点个赞支持支持
前言在面试中,并发线程安全提问必然是不会缺少的,那基础的CAS原理也必须了解,这样在面试中才能加分,那来看看面试可能会问那些问题:什么是乐观锁与悲观锁什么乐观锁的实现方式-CAS(Compare and Swap),CAS(Compare and Swap)实现原理在JDK并发包中的使用CAS的缺陷1. 什么是乐观锁与悲观锁?悲观锁总是假设最坏的情况,每次读取数据的时候都默认其他线程会更改数据,因此需要进行加锁操作,当其他线程想要访问数据时,都需要阻塞挂起。悲观锁的实现:传统的关系型数据库使用这种锁机制,比如行锁,表锁等,读锁,写锁等,都是在做操作之前先上锁;Java里面的同步synchronized关键字的实现。乐观锁乐观锁,其实就是一种思想,总是认为不会产生并发问题,每次读取数据的时候都认为其他线程不会修改数据,所以不上锁,但是在更新的时候会判断一下在此期间别的线程有没有修改过数据,乐观锁适用于读操作多的场景,这样可以提高程序的吞吐量。实现方式:CAS实现:Java中java.util.concurrent.atomic包下面的原子变量使用了乐观锁的一种CAS实现方式,CAS分析看下节。版本号控制:一般是在数据表中加上一个数据版本号version字段,表示数据被修改的次数,当数据被修改时,version值会加一。当线程A要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前数据库中的version值相等时才更新,否则重试更新操作,直到更新成功乐观锁适用于读多写少的情况下(多读场景),悲观锁比较适用于写多读少场景2. 乐观锁的实现方式-CAS(Compare and Swap),CAS(Compare and Swap)实现原理背景在jdk1.5之前都是使用synchronized关键字保证同步,synchronized保证了无论哪个线程持有共享变量的锁,都会采用独占的方式来访问这些变量,导致会存在这些问题:在多线程竞争下,加锁、释放锁会导致较多的上下文切换和调度延时,引起性能问题如果一个线程持有锁,其他的线程就都会挂起,等待持有锁的线程释放锁。如果一个优先级高的线程等待一个优先级低的线程释放锁,会导致优先级倒置,引起性能风险为了优化悲观锁这些问题,就出现了乐观锁:假设没有并发冲突,每次不加锁操作同一变量,如果有并发冲突导致失败,则重试直至成功。CAS(Compare and Swap)原理CAS 全称是 compare and swap(比较并且交换),是一种用于在多线程环境下实现同步功能的机制,其也是无锁优化,或者叫自旋,还有自适应自旋。在jdk中,CAS加volatile关键字作为实现并发包的基石。没有CAS就不会有并发包,java.util.concurrent中借助了CAS指令实现了一种区别于synchronized的一种乐观锁。乐观锁的一种典型实现机制(CAS):乐观锁主要就是两个步骤:冲突检测数据更新当多个线程尝试使用CAS同时更新同一个变量时,只有一个线程可以更新变量的值,其他的线程都会失败,失败的线程并不会挂起,而是告知这次竞争中失败了,并可以再次尝试。在不使用锁的情况下保证线程安全,CAS实现机制中有重要的三个操作数:需要读写的内存位置(V)预期原值(A)新值(B)首先先读取需要读写的内存位置(V),然后比较需要读写的内存位置(V)和预期原值(A),如果内存位置与预期原值的A相匹配,那么将内存位置的值更新为新值B。如果内存位置与预期原值的值不匹配,那么处理器不会做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。具体可以分成三个步骤:读取(需要读写的内存位置(V))比较(需要读写的内存位置(V)和预期原值(A))写回(新值(B))3. CAS在JDK并发包中的使用在JDK1.5以上 java.util.concurrent(JUC java并发工具包)是基于CAS算法实现的,相比于synchronized独占锁,堵塞算法,CAS是非堵塞算法的一种常见实现,使用乐观锁JUC在性能上有了很大的提升。CAS如何在不使用锁的情况下保证线程安全,看并发包中的原子操作类AtomicInteger::getAndIncrement()方法(相当于i++的操作):// AtomicInteger中 //value的偏移量 private static final long valueOffset; //获取值 private volatile int value; //设置value的偏移量 static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } //增加1 public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); } 复制代码首先value必须使用了volatile修饰,这就保证了他的可见性与有序性需要初始化value的偏移量unsafe.getAndAddInt通过偏移量进行CAS操作,每次从内存中读取数据然后将数据进行+1操作,然后对原数据,+1后的结果进行CAS操作,成功的话返回结果,否则重试直到成功为止。//unsafe中 public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { //使用偏移量获取内存中value值 var5 = this.getIntVolatile(var1, var2); //比较并value加+1 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; } 复制代码JAVA实现CAS的原理,unsafe::compareAndSwapInt是借助C来调用CPU底层指令实现的。下面是sun.misc.Unsafe::compareAndSwapInt()方法的源代码:public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x); 复制代码4. CAS的缺陷ABA问题在多线程场景下CAS会出现ABA问题,例如有2个线程同时对同一个值(初始值为A)进行CAS操作,这三个线程如下线程1,期望值为A,欲更新的值为B线程2,期望值为A,欲更新的值为B线程3,期望值为B,欲更新的值为A线程1抢先获得CPU时间片,而线程2因为其他原因阻塞了,线程1取值与期望的A值比较,发现相等然后将值更新为B,这个时候出现了线程3,线程3取值与期望的值B比较,发现相等则将值更新为A此时线程2从阻塞中恢复,并且获得了CPU时间片,这时候线程2取值与期望的值A比较,发现相等则将值更新为B,虽然线程2也完成了操作,但是线程2并不知道值已经经过了A->B->A的变化过程。ABA问题带来的危害:小明在提款机,提取了50元,因为提款机问题,有两个线程,同时把余额从100变为50线程1(提款机):获取当前值100,期望更新为50,线程2(提款机):获取当前值100,期望更新为50,线程1成功执行,线程2某种原因block了,这时,某人给小明汇款50线程3(默认):获取当前值50,期望更新为100,这时候线程3成功执行,余额变为100,线程2从Block中恢复,获取到的也是100,compare之后,继续更新余额为50!!!此时可以看到,实际余额应该为100(100-50+50),但是实际上变为了50(100-50+50-50)这就是ABA问题带来的成功提交。解决方法AtomicStampedReference 带有时间戳的对象引用来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。public boolean compareAndSet( V expectedReference,//预期引用 V newReference,//更新后的引用 int expectedStamp, //预期标志 int newStamp //更新后的标志 ) 复制代码在变量前面加上版本号,每次变量更新的时候变量的版本号都+1,即A->B->A就变成了1A->2B->3A循环时间长开销大自旋CAS(不成功,就一直循环执行,直到成功)如果长时间不成功,会给CPU带来极大的执行开销。解决方法:限制自旋次数,防止进入死循环JVM能支持处理器提供的pause指令那么效率会有一定的提升,只能保证一个共享变量的原子操作当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性解决方法:如果需要对多个共享变量进行操作,可以使用加锁方式(悲观锁)保证原子性,可以把多个共享变量合并成一个共享变量进行CAS操作。各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持! 欢迎扫码关注,原创技术文章第一时间推出各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
前言在 Oracle 数据库中,我们通常在不同数据库的表间记录进行复制或迁移时会用以下几种方法:A表记录利用toad或者pl/sql工具将其导出为一条条分号隔开的insert语句,然后再执行插入到B表中建立数据库间的dblink,然后使用 create table B as select * from A@dblink where...,或者insert into B select * from A@dblink where...exp/expdp A表,再imp/impdp到B表那么除了上面的三种常用的方法,还有其他比较好的方法进行数据迁移吗,下面介绍oracle自带的Sql Loader(sqlldr)的用法。sqlloader简介sqlloaderOracle用于数据迁移、将数据从外部文件加载到Oracle数据库的表中,它具有强大的数据解析引擎,对数据文件中数据的格式几乎没有限制。基本的组成由:*.ctl:控制文件,与表信息相关,数据入表的逻辑处理(数据加载信息,解析数据,导入数据信息)*.bad :执行bat后自动生成,错误日志,保存导入文件出现错误的记录*.log :执行bat后自动生成,日志文件,可以查看导入的情况*.dis:废弃文件常用的参数命令:userid -- ORACLE 用户名/口令control -- 控制文件名,默认 格式 *.ctllog -- 记录导入时的日志文件,默认为 控制文件(去除扩展名).logbad -- 坏数据文件,默认为 控制文件(去除扩展名).baddata -- 数据文件,一般在控制文件中指定。用参数控制文件中不指定数据文件更适于自动操作discard -- 废弃文件名discardmax -- 允许废弃的文件的数目skip -- 要跳过的逻辑记录的数目 (默认 0)load -- 要加载的逻辑记录的数目 (全部默认)rows -- 对于传统常规路径(Conventional Path)导入的情况,代表一次提交(Commit)的行数(默认:6 最大值:65534)bindsize -- 每次提交记录的缓冲区的最大值(仅适用于传统常规路径加载),默认256000 Bytesreadsize -- 读取缓冲区的大小 (适用于传统常规路径和直接路径加载),默认 1048576。errors -- 允许的错误记录数,可以用他来控制一条记录都不能错 (默认 50)silent -- 运行过程中隐藏消息 (标题,反馈,错误,废弃,分区)direct -- 使用直接路径 (默认 FALSE)parfile -- 参数文件: 包含参数说明的文件的名称parallel -- 执行并行加载 (默认 FALSE)file -- 要从以下对象中分配区的文件ROWS对于传统常规路径(Conventional Path)导入的情况,代表一次提交(Commit)的行数(默认:6 最大值:65534)BINDSIZE通过配置BINDSIZE的值,加快加载导入效率,而且配置的值要比默认值和通过参数ROWS计算的缓冲区大小更优先。 即BINDSIZE能够制约ROWS,如果ROWS提交的数据需要的缓冲区大于BINDSIZE的配置值,会以BINDSIZE的设定为准※在设定参数时,一定要同时考虑ROWS和BINDSIZE的设定。READSIZE读取缓冲区的大小 (适用于传统常规路径和直接路径加载),默认 1048576。READSIZE负责读取的缓冲区大小,而BINDSIZE负责提交的缓冲区大小,如果READSIZE小于BINDSIZE,那么READSIZE会自动增加。通过设置READSIZE为更大的值,可以在提交前读取更多的数据到Buffer中去sqlloader安装下载并解压软件地址:www.oracle.com/database/te…下载包:sqlloader所需的基础包:instantclient-basic-windows.x64-19.6.0.0.0dbru.zipsqlloader工具包: instantclient-tools-windows.x64-19.6.0.0.0dbru.zipNOTE:直接下载oracle client客户端即可使用方式使用一个控制文件(*.ctl) 和一个数据文件(*.csv),步骤如下:首先在数据库中创建好需要导入数据的表;创建数据文件,*.csv 文件等类型的文件;创建控制文件 *.ctl,数据入表的逻辑处理。执行sqload命令加载导入数据1). 首先在数据库中创建好需要导入数据的表create table user_info ( userid int, username varchar2(50), address varchar2(500), sex varchar2(2), phone_number varchar2(13) email varchar2(50), certificate_no VARCHAR2(20) )2). 建立数据文件, users_data.csv 文件01412401,李四,广东深圳龙华,M,13444455568,WJ@email.com,31010119850406999901412402,张三,广东深圳龙华,M,13444455567,HH@email.com,31010119850406999801412403,王二,广东深圳福田,M,13444455566,WJ@email.com,31010119850406999701412404,李达,广东深圳南山,M,13444455565,HH@email.com,3101011985040699963). 建立一个控制文件 users_load_data.ctlOPTIONS (skip=1,rows=128) -- sqlldr 命令显示的 选项可以写到这里边来,skip=1 用来跳过数据中的第一行 ,rows=128 代表每128行数--据提交一次 LOAD DATA INFILE "/home/oracle/script/users_data.csv" --指定外部数据文件,可以写多 个 INFILE "another_data_file.csv" 指定多个数据文件 --这里还可以使 用 BADFILE、DISCARDFILE 来指定坏数据和丢弃数据的文件, --truncate --操作类型,用 truncate table 来清除表中原有 记录 append INTO TABLE test_users -- 要插入记录的表,这里插入到另外一张表里 Fields terminated by "," -- 数据中每行记录用 "," 分隔 Optionally enclosed by '"' -- 数据中每个字段用 '"' 框起,比如字段中有 "," 分隔符时 trailing nullcols --表的字段没有对应的值时允 许为空 ( virtual_column FILLER, --这是一个虚拟字段,用来跳 过由 PL/SQL Developer 生成的第一列序号 userid, username, address , phone_number, email , certificate_no ) 复制代码在操作类型 truncate 位置可用以下中的一值:insert :为缺省方式,在数据装载开始时要求表为空append:在表中追加新记录replace:删除旧记录(用 delete from table 语句),替换成新装载的记录truncate :删除旧记录(用 truncate table 语句),替换成新装载的记录时间类型转换字段 DATE "YYYY-MM-DD HH24:MI:SS" -- 指定接受日期的格式,相当用 to_date() 函数转换4).执行命令普通版:在安装好sqlload命令目录中打开CMD命令,然后再命令行窗口执行:sqlldr userid=username/password@ip:port/dbName control=d:\users_load_data.ctl log=d:\userload.log普通导入速度比较慢,一秒才几条,这样导入跟一条条插入数据差不多,因此应该善用其参数,加快加载导入数据升级版:将命令行改成这样:sqlldr userid=username/password@ip:port/dbName control=d:\users_load_data.ctl log=d:\userload.log errors=100000 bindsize=8000000 rows=5000这样可以配置可以在一秒1万条左右,加快导入速度,节省了很多时间。NOTE:当加载海量数据时(大约超过10GB),最好禁止日志的产生,这样不产生REDO LOG,可以提高效率,在 CONTROL 文件中 load data 上面加一行:unrecoverable, 此选项必须要与DIRECT共同应用.对于超大数据文件的导入就要用并发操作了,即同时运行多个导入任务.parallel=true各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
在看这文章之前建议先看看先前架构原理介绍文章:【必须先理解的RocketMQ入门手册,才能再次深入解读】RocketMQ服务器启动linux环境下载编译源码# 下载$ > wget wget http://mirror.bit.edu.cn/apache/rocketmq/4.6.0/rocketmq-all-4.6.0-source- > # 解压$ >unzip rocketmq-all-4.7.0-source-release.zip > cd rocketmq-all-4.7.0/ # 编译$ > mvn -Prelease-all -DskipTests clean install -U > cd distribution/target/rocketmq-4.7.0/rocketmq-4.7.0 复制代码启动 Name Server# 启动 Name Server 服务 > nohup sh bin/mqnamesrv & # 启动完成后,查看日志$ > tail -f ~/logs/rocketmqlogs/namesrv.log The Name Server boot success... 复制代码启动 Broker在 conf 目录下,RocketMQ 提供了多种 Broker 的配置文件:broker.conf :单主,异步刷盘。2m/ :双主,异步刷盘。2m-2s-async/ :两主两从,异步复制,异步刷盘。2m-2s-sync/ :两主两从,同步复制,异步刷盘。dledger/ :Dledger 集群,至少三节点# 启动 Broker服务 > nohup sh bin/mqbroker -n localhost:9876 & # 启动完成后,查看日志$ > tail -f ~/logs/rocketmqlogs/broker.log The broker[%s, 172.30.30.233:10911] boot success... 复制代码其中,参数:通过 -c 参数,配置读取的主 Broker 配置通过 -n 参数,设置 RocketMQ Namesrv 地址关闭服务器> sh bin/mqshutdown broker The mqbroker(36695) is running... Send shutdown request to mqbroker(36695) OK > sh bin/mqshutdown namesrv The mqnamesrv(36664) is running... Send shutdown request to mqnamesrv(36664 复制代码windows环境首先去官网下载编译之后的版本,然后解压到本地目录官网链接:rocketmq.apache.org/dowloading/…下载目标:Binary: [rocketmq-all-4.7.0-bin-release.zip配置ROCKETMQ_HOME到系统环境变量中,启动脚本将读取ROCKETMQ_HOME变量分别进入bin目录下 启动如下脚本(需要设置内存参数,防止内存过大,启动失败,具体看<常出现的错误>小节):3.1 启动namesrv运行命令:mqnamesrv.cmd 复制代码log : The Name Server boot success. serializeType=JSON3.2 启动brokerserver运行命令:mqbroker.cmd -n localhost:9876 复制代码log : The broker[IQSZ-L01898, 10.111.45.111:10911] boot success. serializeType=JSON and name server is localhost:9876关闭服务器mqshutdown.cmd brokerlog:killing name servermqshutdown.cmd namesrvlog:killing brokerRocketMQ发送消息和消费消息RocketMQ发送消息和消费消息,先启动消费者,然后再启动生产者添加依赖<dependency> <groupId>org.apache.rocketmq</groupId> <artifactId>rocketmq-client</artifactId> <version>4.3.0</version> </dependency> 复制代码发送消息发送消息--同步public class SyncProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("test-group"); // Specify name server addresses. producer.setNamesrvAddr("localhost:9876"); //Launch the instance. producer.start(); for (int i = 0; i < 100; i++) { //Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); //Call send message to deliver message to one of brokers. SendResult sendResult = producer.send(msg); System.out.printf("%s%n", sendResult); } //Shut down once the producer instance is not longer in use. producer.shutdown(); } } 复制代码发送消息--异步public class AsyncProducer { public static void main(String[] args) throws Exception { //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("test—group"); // Specify name server addresses. producer.setNamesrvAddr("localhost:9876"); //Launch the instance. producer.start(); producer.setRetryTimesWhenSendAsyncFailed(0); for (int i = 0; i < 100; i++) { final int index = i; //Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTest", "TagA", "OrderID188", "Hello world".getBytes(RemotingHelper.DEFAULT_CHARSET)); producer.send(msg, new SendCallback() { @Override public void onSuccess(SendResult sendResult) { System.out.printf("%-10d OK %s %n", index, sendResult.getMsgId()); } @Override public void onException(Throwable e) { System.out.printf("%-10d Exception %s %n", index, e); e.printStackTrace(); } }); } //Shut down once the producer instance is not longer in use. producer.shutdown(); } 复制代码发送消息--单向public class OnewayProducer { public static void main(String[] args) throws Exception{ //Instantiate with a producer group name. DefaultMQProducer producer = new DefaultMQProducer("please_rename_unique_group_name"); // Specify name server addresses. producer.setNamesrvAddr("localhost:9876"); //Launch the instance. producer.start(); for (int i = 0; i < 100; i++) { //Create a message instance, specifying topic, tag and message body. Message msg = new Message("TopicTest" /* Topic */, "TagA" /* Tag */, ("Hello RocketMQ " + i).getBytes(RemotingHelper.DEFAULT_CHARSET) /* Message body */ ); //Call send message to deliver message to one of brokers. producer.sendOneway(msg); } //Shut down once the producer instance is not longer in use. producer.shutdown(); } } 复制代码消费消息public class Consumer { public static void main(String[] args) throws InterruptedException, MQClientException { // Instantiate with specified consumer group name. DefaultMQPushConsumer consumer = new DefaultMQPushConsumer("test-group"); // Specify name server addresses. consumer.setNamesrvAddr("localhost:9876"); // Subscribe one more more topics to consume. consumer.subscribe("TopicTest", "*"); // Register callback to execute on arrival of messages fetched from brokers. consumer.registerMessageListener(new MessageListenerConcurrently() { @Override public ConsumeConcurrentlyStatus consumeMessage(List<MessageExt> msgs, ConsumeConcurrentlyContext context) { System.out.printf("%s Receive New Messages: %s %n", Thread.currentThread().getName(), msgs); return ConsumeConcurrentlyStatus.CONSUME_SUCCESS; } }); //Launch the consumer instance. consumer.start(); System.out.printf("Consumer Started.%n"); } 复制代码常出现的错误安装中出现的错误防止内存设置过大修改runbroker.cmd配置文件set "JAVA_OPT=%JAVA_OPT% -server -Xms500m -Xmx500m -Xmn500m"set "JAVA_OPT=%JAVA_OPT% -XX:MaxDirectMemorySize=1g"修改runserver.cmd配置文件set "JAVA_OPT=%JAVA_OPT% -server -Xms500m -Xmx500m -Xmn500m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=320m"启动NAMESERVER报错unrecognized vm option 'MetasoaceSize=128m'解决方法:更换jdk版本为1.8即可启动BROKER报错错误: 找不到或无法加载主类 xxxxxx’解决方法:打开runbroker.cmd(windows),然后将‘%CLASSPATH%’加上英文双引使用过程中出现的错误No route info of this topicBroker禁止自动创建Topic,且用户没有通过手工方式创建Topic查看是否允许自动创建topic命令:mqbroker.cmd -n localhost:9876 -pmq开启自动创建topic参数命令:mqbroker.cmd -n localhost:9876 autoCreateTopicEnable=trueBroker 没有正确连接到 Name Server查看broker.log日志位置: /安装目录/conf/logback_broker.xml中日志位置日志信息:broker.log日志信息:namesrv.logProducer 没有正确连接到 Name Serverlinux环境:查询防火墙是否通错误分析方法日志分析法:查看broker日志关注broker是否有注册到nameserverregister broker to name server localhost:9876 OK关注生产者是否连接到brokernew producer connected, group: test_group channel: ClientChannelInfo ... 复制代码查看已经创建的topic是否包含自己想要的topic2020-04-21 15:58:22 INFO main - load exist local topic, TopicConfig [topicName=test_group, readQueueNums=1, writeQueueNums=1, perm=RW-, topicFilterType=SINGLE_TAG, topicSysFlag=0, order=false] ...查看消费者是否连接到brokernew consumer connected, group: test_group CONSUME_PASSIVELY CLUSTERING channel: ClientChannelInfo ...查看nameserver日志关注broker是否注册到nameservernew broker registered, localhost:10911查看topic消息2020-04-21 17:03:32 INFO RemotingExecutorThread_1 - new topic registered, test_topic QueueData [brokerName=broker-a, readQueueNums=8, writeQueueNums=8, perm=6, topicSynFlag=0]...各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
RocketMQ入门手册RocketMQ是一个分布式、队列模型的开源消息中间件,前身是MetaQ,是阿里研发的一个队列模型的消息中间件,后开源给apache基金会成为了apache的顶级开源项目,具有高性能、高可靠、高实时、分布式特点,同时,广泛应用于多个领域,包括异步通信解耦、企业解决方案、金融支付、电信、电子商务、快递物流、广告营销、社交、即时通信、移动应用、手游、视频、物联网、车联网等。具有以下特点:能够保证严格的消息顺序提供丰富的消息拉取模式高效的订阅者水平扩展能力实时的消息订阅机制亿级消息堆积能力RocketMQ 架构原理分析RocketMQ 架构NameServer (名称服务器):提供轻量级的服务发现和路由。NameServer接受来自Broker群集的注册,并提供检测信号机制以检查Broker是否还存在每个NameServer记录完整的路由信息(Broker 相关 Topic 等元信息,并给 Producer 提供 Consumer 查找 Broker 信息),提供相应的读写服务。Broker(消息服务器): 消息存储中心,接收来自 Producer 的消息并存储, Consumer 从这里取得消息单个Broker节点与所有的NameServer节点保持长连接及心跳,并会定时将Topic信息注册到NameServer,(其底层通信是基于Netty实现的)Broker负责消息存储,以Topic为维度支持轻量级的队列,单机可以支撑上万队列规模,支持消息推拉模型。具有上亿级消息堆积能力,同时可严格保证消息的有序性Producer (生产者):负责产生消息,生产者向消息服务器发送由业务应用程序系统生成的消息生产者支持分布式部署。 分布式生产者通过多种负载平衡模式将消息发送到Broker集群。 发送过程支持快速失败并且延迟低三种方式发送消息:同步、异步和单向Consumer(消费者):负责消费消息,消费者从消息服务器拉取信息并将其输入用户应用程序也支持“推和拉”模型中的分布式部署。它还支持集群使用和消息广播。 它提供了实时消息订阅机制,可以满足大多数消费者的需求。Broker ServerBroker Server负责消息的存储和传递,消息查询,HA高可用等,Broker Server几个主要模块组成:Remoting Module(远程模块):broker入口,处理来自客户端的请求Client Manager(客户端管理):管理client(生产者/消费者)并维护消费者的主题订阅Store Service(存储服务):提供简单的API中数据库中存储或查询消息HA Service(高可用服务):提供master broker和slave broker之间的数据同步功能Index Service(索引服务):将message建立索引来提供快速的查询能力RocketMQ 整体流程启动 NameServer,NameServer启动后进行端口监听,等待 Broker、Producer、Consumer 连上来,相当于一个路由控制中心Broker 启动,跟所有的 Namesrv 保持长连接,定时发送心跳包心跳包中,包含当前 Broker 信息(IP+端口等)以及存储所有 Topic 信息注册成功后,Namesrv 集群中就有 Topic 跟 Broker 的映射关系收发消息前,先创建 Topic 。创建 Topic 时,需要指定该 Topic 要存储在哪些 Broker上。也可以在发送消息时自动创建TopicProducer 发送消息启动时,先跟 Namesrv 集群中的其中一台建立长连接,并从Namesrv 中获取当前发送的 Topic 存在哪些 Broker 上然后跟对应的 Broker 建立长连接,直接向 Broker 发消息Consumer 消费消息跟其中一台 Namesrv 建立长连接,获取当前订阅 Topic 存在哪些 Broker 上然后直接跟 Broker 建立连接通道,开始消费消息*RocketMQ的消息领域模型RocketMQ MessageTopic(主题): 表示消息的第一级类型,是最细粒度的订阅单位(生产者传递消息和消费者提取消息标识)一条消息必须有一个Topic一个Group可以订阅多个Topic的消息Topic一般为领域范围,比如交易消息Tag(标签): 表示消息的第二级类型,可以是使用相同的Topic不同的Tag来表示同一业务模块的不同任务的消息,比如交易消息又可以分为:交易创建消息,交易完成消息等助于保持代码整洁和一致简化RocketMQ提供的查询系统Message(消息体): 消息是要传递的信息。 Message中必须包含一个Topic,可选Tag和key-vaule键值对Message Queue(消息队列): 所有消息队列都是持久化一个Topic下可以有多个QueueQueue的引入使得消息的存储可以分布式集群化,具有了水平扩展能力Group(组): 分为Producer Group(生产者组)和Consumer Group(消费者组),具有相同角色组成Group原生产者在交易后崩溃,broker可以联系同一生产者组的不同生产者实例以进行提交或回退交易。消费者组的消费者实例必须具有完全相同的主题订阅RocketMQ 特性Message Model(消息模式):Clustering(集群式):当使用集群消费模式时,MQ 认为任意一条消息只需要被集群内的任意一个消费者处理即可Broadcasting(广播式):当使用广播消费模式时,MQ 会将每条消息推送给集群内所有注册过的客户端,保证消息至少被每台机器消费一次Message Order(消息顺序)使用DefaultMQPushConsumer时,可以决定按顺序或同时使用消息Orderly:有序地使用消息意味着消息的消费顺序与生产者为每个消息队列发送消息的顺序相同。( 如果要处理必须强制执行全局顺序的情况,请确保您使用的主题只有一个消息队列)如果指定按顺序使用,则消息使用的最大并发度是使用者组订阅的消息队列数Concurrently:同时使用消息时,消息使用的最大并发性仅受为每个使用方客户端指定的线程池限制在此模式下不再保证消息顺序Message Types(消息类型)事务消息顺序消息延迟消息RocketMQ单机版安装下载编译源码# 下载$ > wget wget http://mirror.bit.edu.cn/apache/rocketmq/4.6.0/rocketmq-all-4.6.0-source- > # 解压$ >unzip rocketmq-all-4.7.0-source-release.zip > cd rocketmq-all-4.7.0/ # 编译$ > mvn -Prelease-all -DskipTests clean install -U > cd distribution/target/rocketmq-4.7.0/rocketmq-4.7.0 复制代码启动 Name Server# 启动 Name Server 服务 > nohup sh bin/mqnamesrv & # 启动完成后,查看日志$ > tail -f ~/logs/rocketmqlogs/namesrv.log The Name Server boot success... 复制代码启动 Broker在 conf 目录下,RocketMQ 提供了多种 Broker 的配置文件:broker.conf :单主,异步刷盘。2m/ :双主,异步刷盘。2m-2s-async/ :两主两从,异步复制,异步刷盘。2m-2s-sync/ :两主两从,同步复制,异步刷盘。dledger/ :Dledger 集群,至少三节点# 启动 Broker服务 > nohup sh bin/mqbroker -n localhost:9876 & # 启动完成后,查看日志$ > tail -f ~/logs/rocketmqlogs/broker.log The broker[%s, 172.30.30.233:10911] boot success... 复制代码其中,参数:通过 -c 参数,配置读取的主 Broker 配置通过 -n 参数,设置 RocketMQ Namesrv 地址Send & Receive Messages(消息发送与接收)在发送/接收消息之前,我们需要告知client(生产者/消费者)Name Servers的地址。 RocketMQ提供了多种方法来实现:在代码中设置:producer.setNamesrvAddr("ip:port")java属性配置:rocketmq.namesrv.addr环境变量配置:NAMESRV_ADDRHTTP Endpoint为简单起见,我们使用环境变量:NAMESRV_ADDR,如下所示:# 设置 Name Servers的地址$ > export NAMESRV_ADDR=localhost:9876 # 生产消息$ > sh bin/tools.sh org.apache.rocketmq.example.quickstart.Producer SendResult [sendStatus=SEND_OK, msgId= ... # 消费消息$ > sh bin/tools.sh org.apache.rocketmq.example.quickstart.Consumer ConsumeMessageThread_%d Receive New Messages: [MessageExt... 复制代码各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
前言太久没有更新技术博客,后续还是保持以前的更新速度,走向新的学习之路,也欢迎大家一起来学习学习。最后捞一下以前发的面试文章总结,后续将继续更新:【面试宝典】:检验是否为合格的初中级程序员的面试知识点,你都知道了吗?查漏补缺《面试知识,工作可待:集合篇》-java集合面试知识大全java多线程并发系列--基础知识点(笔试、面试必备)【面试官之你说我听】-MyBatis常见面试题赶紧收藏起MongoDB面试题轻松面对BAT灵魂式的拷问推荐收藏系列:一文理解JVM虚拟机(内存、垃圾回收、性能优化)解决面试中遇到问题一、分区表简介1.1 什么是分区表?分区表是将大表的数据分成称为分区的许多小的子集,分区表的种类划分主要有:range(范围)、list(列表)和hash(散列)分区。划分依据主要是根据其表内部属性。分区表可以创建其独特的分区索引,分区表可以从物理上将一个大表分成几个小表,但是从逻辑上来看,还是一个大表。1.2 什么情况下使用分区表呢?表内的数据量很大的时候,影响到业务/技术方容忍的最大查询时间。但数据量并不是判断是否需要创建分区表的惟一条件,如果表内的数据都是基础数据、其数据查询都频率高,这样不建议使用分区表。通常情况下,可以将数据进行分段处理。表的大小超过2GB可进去分区表改造1.3 为什么使用分区表改善查询性能:对分区对象的查询可以仅搜索自己关心的分区,提高检索速度。增强可用性:如果表的某个分区出现故障,表在其他分区的数据仍然可用;维护方便:如果表的某个分区出现故障,需要修复数据,只修复该分区即可;均衡I/O:可以把不同的分区映射到磁盘以平衡I/O,改善整个系统性能。1.4 表分区的类型范围分区(range):基于一个范围将表的数据分配到其所属的分区内。如果需要将行映射到基于列值范围的分区时,就使用范围分区方法--条件是数据可以被划分成逻辑范围;当数据在整个范围内能被均等地划分时性能最好,明显不能均分时须使用其他分区方式“范围”是在创建分区表时指定的分区键决定的,分区方式是最为常用的,并且分区键经常采用日期列表分区(list):基于列某个特性分配其所属的分区该分区的特点是某列的值只有几个,基于这样的特点我们可以采用列表分区散列分区(hash):在列值上使用散列算法来分配其所属分区当列的值没有合适的条件时,建议使用散列分区,通过在I/O设备上进行散列分区,使得这些分区大小一致。二、分区表改造方案这次主要讨论的是以范围分区(range),并且以日期作为分区键。2.1 分区改造前准备在做表分区前,需要表统计分析,各个表、索引空间存储大小,每年或者每个月表的增长率等(可以找DBA)。这边提供DBA常用系统表,视图——docs.oracle.com/cd/B14117_0…2.1.1 统计各个表空间大小select t.owner,t.segment_name,t.tablespace_name,sum(bytes/1024/1024/1024) gb from dba_segments t where t.segment_name in (select t2.OBJECT_NAME from dba_objects t2 where t2.OBJECT_TYPE = 'TABLE' AND t2.owner=upper('tabel_owner') ) group by t.owner,t.segment_name,t.tablespace_name order by 4 desc; 复制代码2.1.2 统计表的索引大小select round(sum(bytes) / 1024 / 1024 / 1024, 4) IDX_GB --表上索引对象占用空间 from dba_segments where owner || segment_name in(select owner || index_name from dba_indexes where table_owner = upper('table_name') and table_name = upper('table_owner')); 复制代码2.2分区表改造步骤前面表的各项指标都分析统计出来,那就开始实际操作起来,首先进行的小于100G的改造方案:创建与原表同构的分区新表将原表设置成read only使用Insert..select from的方式,将原表数据导入到分区新表创建分区新表的索引和约束rename源表和新表的索引名和约束名称,交换命名删除源表的同义词rename源表和分区新表的表名,交换命名创建源表和新表的同义词给rename后的分区新表授权将rename后的源表设置为read write.Note:在进行接下来的脚本的时候记得定义好所需的变量declare v_table_name varchar2(100) := upper('表名'); ----建表变量 v_sql_temp1 varchar2(1000); v_sql_temp2 varchar2(1000); v_sql_temp3 varchar2(1000); ----属主变量 v_owner varchar2(100) := upper('表属主'); -----输出变量 type remark_list is varray(60) of varchar2(3000); v_output_list remark_list; -----授权变量 v_grantee varchar2(100); v_grant_sql varchar2(1000); type type_array is table of varchar(20) index by binary_integer; grantee_list type_array;2.2.1 创建与原表同构的分区新表步骤如下:创建分区表添加字段的默认值添加表以及字段的注释创建分区表创建分区表--脚本生成模板1:罗列所有字段模板,涉及到dba_tab_columns(表的列信息)-----------1.新建分区临时表 v_sql_temp1 := 'CREATE TABLE ' || v_owner || '.' || v_table_name || '_P( '; select COLUMN_NAME ||' '|| decode( DATA_TYPE,'DATE',DATA_TYPE, DATA_TYPE||'('|| DATA_LENGTH || ')') || decode(NULLABLE,'N',' not null','') || ',' bulk collect into v_output_list from dba_tab_columns t where table_name =v_table_name order by t.COLUMN_ID ; dbms_output.put_line(v_sql_temp1); for i in 1 ..v_output_list.count loop --去掉数组中的最后一个字段字符中的逗号"," if i = v_output_list.count then select REPLACE(v_output_list(i),',',' ') into v_sql_temp2 from dual; dbms_output.put_line(v_sql_temp2); else dbms_output.put_line(v_output_list(i)); end if; end loop; --输出分区信息 v_sql_temp3 := ')PARTITION BY RANGE (ARCHIVE_DATE) INTERVAL (NUMTOYMINTERVAL(1, ''YEAR'')) (PARTITION ' || v_table_name || '_2019' || ' VALUES LESS THAN (TO_DATE(''2020-01-01'', ''YYYY-MM-DD'')))ENABLE ROW MOVEMENT MONITORING INITRANS 6;'; dbms_output.put_line(v_sql_temp3); 复制代码生成的模板:CREATE TABLE PASDATA.CLM_PERSON_HOSPITAL_P( CREATED_BY VARCHAR2(100) not null, CREATED_DATE DATE not null, UPDATED_BY VARCHAR2(100) not null, UPDATED_DATE DATE not null, ID_CLM_PERSON_HOSPITAL VARCHAR2(32) not null, REPORT_NO VARCHAR2(30) not null, ID_CLM_CHANNEL_PROCESS VARCHAR2(32) not null, CASE_TIMES NUMBER(22) not null, HOSPITAL_CODE VARCHAR2(22), HOSPITAL_NAME VARCHAR2(100), SUBJECT_CODE VARCHAR2(20), BED_CODE VARCHAR2(100), START_DATE DATE, END_DATE DATE, MIGRATE_FROM VARCHAR2(1), AFFIRM_SIGN VARCHAR2(1) not null, DOCUMENT_GROUP_ID VARCHAR2(30), HOSPITALIZATION_NUMBER VARCHAR2(30), HOSPITALIZE_DAYS NUMBER(22), ARCHIVE_DATE DATE )PARTITION BY RANGE (ARCHIVE_DATE) INTERVAL (NUMTOYMINTERVAL(1, 'YEAR')) ( PARTITION CLM_PERSON_HOSPITAL_2018 VALUES LESS THAN (TO_DATE('2019-01-01', 'YYYY-MM-DD')) )ENABLE ROW MOVEMENT MONITORING INITRANS 6; 复制代码创建分区表--脚本生成模板2:declare v_table_name varchar2(100) := upper('表名'); -------建表变量 v_sql_temp1 varchar2(1000); v_sql_temp2 varchar2(1000); v_sql_partion varchar2(1000); ----属主变量 v_owner varchar2(100) := upper('表属主'); -----输出变量 如果字段数量超过60个,修改数组大小即可 type remark_list is varray(60) of varchar2(3000); v_remark_list remark_list; begin ----在begin后面加上DBMS_OUTPUT.ENABLE(buffer_size => null) ,表示输出buffer不受限制。 DBMS_OUTPUT.ENABLE(buffer_size => null); ----------------------------------------1.新建分区临时表---------------------------------- v_sql_temp1 := 'CREATE TABLE ' || v_owner || '.' || v_table_name || '_P PARTITION BY RANGE (ARCHIVE_DATE) INTERVAL (NUMTOYMINTERVAL(1, ''YEAR'')) ('; v_sql_temp2 := 'PARTITION ' || v_table_name || '_2019' || ' VALUES LESS THAN (TO_DATE(''2020-01-01'', ''YYYY-MM-DD''))'; v_sql_temp3 := ')ENABLE ROW MOVEMENT MONITORING INITRANS 6 AS SELECT * FROM ' || v_owner || '.' || v_table_name || ' WHERE 1=0;'; dbms_output.put_line(v_sql_temp1 || v_sql_temp2 || v_sql_temp3); 复制代码生成的模板:CREATE TABLE PASDATA.EDR_APPLY_PLAN_INFO_P PARTITION BY RANGE (ARCHIVE_DATE) INTERVAL (NUMTOYMINTERVAL(1, 'YEAR')) ( PARTITION PART_BEFORE_2018 VALUES LESS THAN (TO_DATE('2018-01-01', 'YYYY-MM-DD')) )ENABLE ROW MOVEMENT MONITORING INITRANS 6 AS SELECT * FROM PASDATA.EDR_APPLY_PLAN_INFO WHERE 1=0; 复制代码添加分区表字段的默认值------- 2.对分区新表字段增加默认值 select 'alter table ' || t.owner || '.' || t.table_name || '_NEW modify ' || t.column_name || ' default 记得填默认值;' bulk collect into v_output_list from DBA_TAB_COLS t where t.TABLE_NAME = v_table_name and t.owner = v_owner and t.data_default is not null; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码添加表以及字段的注释添加表以及字段的注释涉及到表 dba_tab_comments(表注释信息)、dba_col_comments(列注释信息)------------- 3.对分区新表的表名,字段增加注释 select 'comment on table ' || a.owner || '.' || a.table_name || '_P is ''' || a.comments || ''';' bulk collect into v_output_list from dba_tab_comments a where a.table_name = upper(v_table_name) and a.owner = upper(v_owner); for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; select 'comment on column ' || owner || '.' || table_name || '_P.' || column_name || ' is ' || '''' || comments || ''';' bulk collect into v_output_list from dba_col_comments where table_name = v_table_name; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码2.2.3 使用Insert..select from的方式,将原表数据导入到分区新表dbms_output.put_line('insert /*+ append parallel(A, 4) */ into ' || v_owner || '.' || v_table_name || '_P A select /*+ parallel(T, 4) */ * from ' || v_owner || '.' || v_table_name || ' T;'); dbms_output.put_line('commit;'); 复制代码2.2.4 创建分区新表的索引和约束创建分区表索引创建分区表索引涉及到dba_indexes (用户模式的索引信息)、dba_ind_columns( 索引与表字段的相关信息)select 'create ' || decode(a.uniqueness, 'UNIQUE', 'UNIQUE', '') || ' index ' || a.owner || '.' || a.index_name || '_N on ' || a.table_owner || '.' || a.table_name || '_P (' || (select wm_concat(b.column_name) from dba_ind_columns b where b.index_name = a.index_name and b.table_owner = v_owner) || ') initrans 16 PARALLEL 8 online;' bulk collect into v_output_list from dba_indexes a where a.table_name = v_table_name and a.index_type != 'LOB'; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码创建表约束创建分区表索约束涉及到dba_cons_columns(数据库所有列的约束信息)、dba_constraints( 数据库中所有表的所有约束定义),当dba_constraints中的constraint_type值为为p时为表主键,值为R时为外键。创建主键约束----创建主键约束 select 'ALTER TABLE ' || a.owner || '.' || a.table_name || '_P ADD CONSTRAINT ' || a.constraint_name || '_N PRIMARY KEY (' || a.column_name || ');' bulk collect into v_output_list from dba_cons_columns a where a.constraint_name = (select constraint_name from dba_constraints b where b.table_name = v_table_name and b.owner = a.owner and constraint_type = 'P') and a.owner = v_owner; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码创建外键约束-----(如果有外键的话,创建外键约束) select 'alter table ' || a.owner || '.' || a.table_name || '_P add constraint ' || a.constraint_name || '_N foreign key(' || b.column_name || ') references ' || (select c.owner || '.' || c.table_name || '(' || c.column_name || ')' from dba_cons_columns c, dba_constraints d where c.constraint_name = d.constraint_name and d.constraint_type = 'P' and c.constraint_name = a.r_constraint_name and c.owner = v_owner and d.owner = v_owner) || ';' bulk collect into v_output_list from dba_constraints a, dba_cons_columns b where a.constraint_name = b.constraint_name and a.table_name = v_table_name and a.constraint_type = 'R'; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码2.2.5 rename源表和新表的索引名和约束名称以及表名将原表索引、表约束变更为临时索引,约束select 'alter index ' || a.owner || '.' || a.index_name || ' rename to ' || a.index_name || '_T;' bulk collect into v_output_list from dba_indexes a where a.table_name = v_table_name and a.index_type != 'LOB'; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; select 'alter table ' || a.owner || '.' || a.table_name || ' rename constraint ' || a.constraint_name || ' to ' || a.constraint_name || '_T;' bulk collect into v_output_list from dba_constraints a where a.table_name = v_table_name and a.constraint_type in ('P', 'R'); for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码将分区表的索引名、表约束变更为原表的索引、约束select 'alter index ' || a.owner || '.' || a.index_name || '_N rename to ' || a.index_name || ';' bulk collect into v_output_list from dba_indexes a where a.table_name = v_table_name and a.index_type != 'LOB'; for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; select 'alter table ' || a.owner || '.' || a.table_name || '_P rename constraint ' || a.constraint_name || '_N to ' || a.constraint_name || ';' bulk collect into v_output_list from dba_constraints a where a.table_name = v_table_name and a.constraint_type in ('P', 'R'); for i in 1 .. v_output_list.count loop dbms_output.put_line(v_output_list(i)); end loop; 复制代码2.2.6 给rename后的分区新表授权给rename后的分区新表授权涉及到dba_tab_privs(数据库所有列的授权信息),查询所有的授权列表进行输出,定义好grantee_list、v_grant_sql等变量select distinct (t.grantee) bulk collect into grantee_list from dba_tab_privs t where (t.owner = upper(v_owner)) and t.table_name = v_table_name; for i in 1 .. grantee_list.count loop v_grantee := grantee_list(i); ``` select 'grant ' || (select wm_concat(t.privilege) from dba_tab_privs t where t.table_name = v_table_name and t.grantee = v_grantee) || ' on ' || t.owner || '.' || t.table_name || ' to ' || t.grantee || ';' into v_grant_sql from dba_tab_privs t where t.table_name = v_table_name and t.grantee = v_grantee and rownum = 1; dbms_output.put_line(v_grant_sql); end loop; 复制代码2.3 分区表改造完成当分区表的改造完成后保险地进行验证一下,数据量,索引,授权列表对比索引select * from dba_indexes a where a.table_name = 'CLM_HIS_RECIPE_DETAIL_NEW' and a.index_type != 'LOB'; 复制代码对比授权用户列表select * from dba_tab_privs t where t.table_name in ('CLM_PERSON_HOSPITAL_NEW') and t.owner = 'CLAIMDATA'; 复制代码各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
前言你和任何一个陌生人之间所间隔的人不会超过六个即最多通过6个中间人你就能够认识任何一个陌生人对于一个社交网络APP,一定会存在着错综复杂的用户关系以及用户属性,在数据库表的设计中除了要存储每个用户的姓名、性别、喜好这些基本信息外,还需要存储一个用户和哪些用户是朋友 ,和哪些用户是亲人等这些关系数据的用户关系,那Neo4j图数据库就该出场了。那Neo4j图数据库是什么呢?Neo4j是一个高性能的,NOSQL数据库,它将结构化数据存储在网络上而不是表中。它是一个嵌入式的、基于磁盘的、具备完全的事物特性的java持久化引擎。Neo4j也可以看作是一个高性能的图引擎,该引擎具有成熟数据库所有特性。Neo4j图数据库适用的场景社交媒体和社交网络图: 以Neo4j图形数据库为基础,社交网络APP可以轻松处理社交关系或根据活动推断关系。知识图谱: 基于Neo4j数据类型以及图形的强大搜索功能, 由知识点之间的关系建立知识图,帮助用户搜索到关联的知识 。反欺诈多维关联分析: 通过图分析可以清楚的知道洗钱网络及相关嫌疑,例如对用户所使用的账号、发生交易时的IP地址、MAC地址、手机IMEI号等进行关联分析企业关系图谱: 企业在日常经营中,与客户、合作伙伴、渠道方、投资者都会打交道,这也决定了企业对社会各个领域都广有涉猎,呈现面错综复杂,因此可以通过Neo4j的企业数据图谱来查询,层层挖掘信息。推荐:基于Neo4j的优势进行个性化推荐, 通过分析用户有哪些朋友、用户朋友喜好的产品、用户的浏览记录等关系信息推测用户的喜好进而为用户推荐商品...那为什么需要Neo4j图数据?它很容易表示连接的数据检索/遍历/导航更多的连接数据是非常容易和快速的它非常容易地表示半结构化数据Neo4j CQL查询语言命令是人性化的可读格式,非常容易学习使用简单而强大的数据模型它不需要复杂的连接来检索连接的/相关的数据,因为它很容易检索它的相邻节点或关系细节没有连接或索引那Neo4j怎么存储错综复杂的用户关系?Neo4j图数据库遵循属性图模型来存储和管理其数据 ,其中属性图模型规则表示节点,关系和属性中的数据节点和关系都包含属性关系连接节点属性是键值对节点用圆圈表示,关系用方向键表示。关系具有方向:单向和双向。每个关系包含“开始节点”或“从节点”和“到节点”或“结束节点”在属性图数据模型中,关系应该是定向的, 关系也应该是有方向性的Neo4j使用本机GPE(图形处理引擎)引擎来使用它的本机图存储格式属性图模型 主要构建块节点:图表的基本单位。 它包含具有键值对的属性关系: 连接两个节点 , 每个关系包含一个起始节点和一个结束节点 , 关系也可以包含属性作为键值对属性:用于描述图节点和关系的键值对标签 : Label将一个公共名称与一组节点或关系相关联。 节点或关系可以包含一个或多个标签数据浏览器 :一旦我们安装Neo4j,我们可以访问Neo4j数据浏览器使用以下URLhttp:// localhost:7474 / browser /使用圆圈表示节点。 使用箭头的关系。 关系是有方向性的。 我们可以用Properties(键值对)来表示Node的数据。在上图中,社交网络图包含为“人”的数据节点,分别代表五个用户。每个数据节点还包含人的基本属性信息等等,用于表示两个用户的基本信息,就如同常规数据库中的两行数据。每两个数据节点之间关系数据,如Ccww与Ccww1的用户是父子关系。 利用这些关系数据,你就可以方便的作出基于关系的查询,这就是图数据库的优势。标签man和woman,可以为现有节点或关系创建新标签,也可以从现有节点或关系中删除现有标签, 两个节点之间的关系,也有一个标签 。那么Neo4j操作命令呢?Neo4j - CQL代表Cypher查询语言。 像Oracle数据库具有查询语言SQL,Neo4j具有CQL作为查询语言:它是Neo4j图形数据库的查询语言。它是一种声明性模式匹配语言它遵循SQL语法。它的语法是非常简单且人性化、可读的格式。常用的Neo4j CQL命令CQL命令/条用法CREATE 创建创建节点,关系和属性MATCH 匹配检索有关节点,关系和属性数据RETURN 返回返回查询结果WHERE 哪里提供条件过滤检索数据DELETE 删除删除节点和关系REMOVE 移除删除节点和关系的属性ORDER BY以…排序排序检索数据SET 组添加或更新标签Neo4j CQL 函数定制列表功能用法String 字符串它们用于使用String字面量。Aggregation 聚合它们用于对CQL查询结果执行一些聚合操作。Relationship 关系他们用于获取关系的细节,如startnode,endnode等。Neo4j CQL数据类型CQL数据类型用法boolean用于表示布尔文字:true,false。byte用于表示8位整数。short用于表示16位整数。int用于表示32位整数。long用于表示64位整数。floatI用于表示32位浮点数。double用于表示64位浮点数。char用于表示16位字符。String用于表示字符串。各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
在微服务中,rest服务互相调用是很普遍的,我们该如何优雅地调用,其实在Spring框架使用RestTemplate类可以优雅地进行rest服务互相调用,它简化了与http服务的通信方式,统一了RESTful的标准,封装了http链接,操作使用简便,还可以自定义RestTemplate所需的模式。其中:RestTemplate默认使用HttpMessageConverter实例将HTTP消息转换成POJO或者从POJO转换成HTTP消息。默认情况下会注册主mime类型的转换器,但也可以通过setMessageConverters注册自定义转换器。RestTemplate使用了默认的DefaultResponseErrorHandler,对40X Bad Request或50X internal异常error等错误信息捕捉。RestTemplate还可以使用拦截器interceptor,进行对请求链接跟踪,以及统一head的设置。其中,RestTemplate还定义了很多的REST资源交互的方法,其中的大多数都对应于HTTP的方法,如下:方法解析delete()在特定的URL上对资源执行HTTP DELETE操作exchange()在URL上执行特定的HTTP方法,返回包含对象的ResponseEntityexecute()在URL上执行特定的HTTP方法,返回一个从响应体映射得到的对象getForEntity()发送一个HTTP GET请求,返回的ResponseEntity包含了响应体所映射成的对象getForObject()发送一个HTTP GET请求,返回的请求体将映射为一个对象postForEntity()POST 数据到一个URL,返回包含一个对象的ResponseEntitypostForObject()POST 数据到一个URL,返回根据响应体匹配形成的对象headForHeaders()发送HTTP HEAD请求,返回包含特定资源URL的HTTP头optionsForAllow()发送HTTP OPTIONS请求,返回对特定URL的Allow头信息postForLocation()POST 数据到一个URL,返回新创建资源的URLput()PUT 资源到特定的URL1. RestTemplate源码1.1 默认调用链路restTemplate进行API调用时,默认调用链:###########1.使用createRequest创建请求######## resttemplate->execute()->doExecute() HttpAccessor->createRequest() //获取拦截器Interceptor,InterceptingClientHttpRequestFactory,SimpleClientHttpRequestFactory InterceptingHttpAccessor->getRequestFactory() //获取默认的SimpleBufferingClientHttpRequest SimpleClientHttpRequestFactory->createRequest() #######2.获取响应response进行处理########### AbstractClientHttpRequest->execute()->executeInternal() AbstractBufferingClientHttpRequest->executeInternal() ###########3.异常处理##################### resttemplate->handleResponse() ##########4.响应消息体封装为java对象####### HttpMessageConverterExtractor->extractData() 复制代码1.2 restTemplate->doExecute()在默认调用链中,restTemplate 进行API调用都会调用 doExecute 方法,此方法主要可以进行如下步骤:1)使用createRequest创建请求,获取响应2)判断响应是否异常,处理异常3)将响应消息体封装为java对象@Nullable protected <T> T doExecute(URI url, @Nullable HttpMethod method, @Nullable RequestCallback requestCallback, @Nullable ResponseExtractor<T> responseExtractor) throws RestClientException { Assert.notNull(url, "URI is required"); Assert.notNull(method, "HttpMethod is required"); ClientHttpResponse response = null; try { //使用createRequest创建请求 ClientHttpRequest request = createRequest(url, method); if (requestCallback != null) { requestCallback.doWithRequest(request); } //获取响应response进行处理 response = request.execute(); //异常处理 handleResponse(url, method, response); //响应消息体封装为java对象 return (responseExtractor != null ? responseExtractor.extractData(response) : null); }catch (IOException ex) { String resource = url.toString(); String query = url.getRawQuery(); resource = (query != null ? resource.substring(0, resource.indexOf('?')) : resource); throw new ResourceAccessException("I/O error on " + method.name() + " request for \"" + resource + "\": " + ex.getMessage(), ex); }finally { if (response != null) { response.close(); } } } 复制代码1.3 InterceptingHttpAccessor->getRequestFactory()在默认调用链中,InterceptingHttpAccessor的getRequestFactory()方法中,如果没有设置interceptor拦截器,就返回默认的SimpleClientHttpRequestFactory,反之,返回InterceptingClientHttpRequestFactory的requestFactory,可以通过resttemplate.setInterceptors设置自定义拦截器interceptor。//Return the request factory that this accessor uses for obtaining client request handles. public ClientHttpRequestFactory getRequestFactory() { //获取拦截器interceptor(自定义的) List<ClientHttpRequestInterceptor> interceptors = getInterceptors(); if (!CollectionUtils.isEmpty(interceptors)) { ClientHttpRequestFactory factory = this.interceptingRequestFactory; if (factory == null) { factory = new InterceptingClientHttpRequestFactory(super.getRequestFactory(), interceptors); this.interceptingRequestFactory = factory; } return factory; } else { return super.getRequestFactory(); } } 复制代码然后再调用SimpleClientHttpRequestFactory的createRequest创建连接:@Override public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { HttpURLConnection connection = openConnection(uri.toURL(), this.proxy); prepareConnection(connection, httpMethod.name()); if (this.bufferRequestBody) { return new SimpleBufferingClientHttpRequest(connection, this.outputStreaming); } else { return new SimpleStreamingClientHttpRequest(connection, this.chunkSize, this.outputStreaming); } } 复制代码1.4 resttemplate->handleResponse()在默认调用链中,resttemplate的handleResponse,响应处理,包括异常处理,而且异常处理可以通过调用setErrorHandler方法设置自定义的ErrorHandler,实现对请求响应异常的判别和处理。自定义的ErrorHandler需实现ResponseErrorHandler接口,同时Spring boot也提供了默认实现DefaultResponseErrorHandler,因此也可以通过继承该类来实现自己的ErrorHandler。DefaultResponseErrorHandler默认对40X Bad Request或50X internal异常error等错误信息捕捉。如果想捕捉服务本身抛出的异常信息,需要通过自行实现RestTemplate的ErrorHandler。ResponseErrorHandler errorHandler = getErrorHandler(); //判断响应是否有异常 boolean hasError = errorHandler.hasError(response); if (logger.isDebugEnabled()) { try { int code = response.getRawStatusCode(); HttpStatus status = HttpStatus.resolve(code); logger.debug("Response " + (status != null ? status : code)); }catch (IOException ex) { // ignore } } //有异常进行异常处理 if (hasError) { errorHandler.handleError(url, method, response); } } 复制代码1.5 HttpMessageConverterExtractor->extractData()在默认调用链中, HttpMessageConverterExtractor的extractData中进行响应消息体封装为java对象,就需要使用message转换器,可以通过追加的方式增加自定义的messageConverter:先获取现有的messageConverter,再将自定义的messageConverter添加进去。根据restTemplate的setMessageConverters的源码可得,使用追加的方式可防止原有的messageConverter丢失,源码:public void setMessageConverters(List<HttpMessageConverter<?>> messageConverters) { //检验 validateConverters(messageConverters); // Take getMessageConverters() List as-is when passed in here if (this.messageConverters != messageConverters) { //先清除原有的messageConverter this.messageConverters.clear(); //后加载重新定义的messageConverter this.messageConverters.addAll(messageConverters); } } 复制代码HttpMessageConverterExtractor的extractData源码:MessageBodyClientHttpResponseWrapper responseWrapper = new MessageBodyClientHttpResponseWrapper(response); if (!responseWrapper.hasMessageBody() || responseWrapper.hasEmptyMessageBody()) { return null; } //获取到response的ContentType类型 MediaType contentType = getContentType(responseWrapper); try { //依次循环messageConverter进行判断是否符合转换条件,进行转换java对象 for (HttpMessageConverter<?> messageConverter : this.messageConverters) { //会根据设置的返回类型responseType和contentType参数进行匹配,选择合适的MessageConverter if (messageConverter instanceof GenericHttpMessageConverter) { GenericHttpMessageConverter<?> genericMessageConverter = (GenericHttpMessageConverter<?>) messageConverter; if (genericMessageConverter.canRead(this.responseType, null, contentType)) { if (logger.isDebugEnabled()) { ResolvableType resolvableType = ResolvableType.forType(this.responseType); logger.debug("Reading to [" + resolvableType + "]"); } return (T) genericMessageConverter.read(this.responseType, null, responseWrapper); } } if (this.responseClass != null) { if (messageConverter.canRead(this.responseClass, contentType)) { if (logger.isDebugEnabled()) { String className = this.responseClass.getName(); logger.debug("Reading to [" + className + "] as \"" + contentType + "\""); } return (T) messageConverter.read((Class) this.responseClass, responseWrapper); } } } } ..... } 复制代码1.6 contentType与messageConverter之间的关系在HttpMessageConverterExtractor的extractData方法中看出,会根据contentType与responseClass选择messageConverter是否可读、消息转换。关系如下:类名支持的JavaType支持的MediaTypeByteArrayHttpMessageConverterbyte[]application/octet-stream, */*StringHttpMessageConverterStringtext/plain, */*ResourceHttpMessageConverterResource*/*SourceHttpMessageConverterSourceapplication/xml, text/xml, application/*+xmlAllEncompassingFormHttpMessageConverterMap<K, List<?>>application/x-www-form-urlencoded, multipart/form-dataMappingJackson2HttpMessageConverterObjectapplication/json, application/*+jsonJaxb2RootElementHttpMessageConverterObjectapplication/xml, text/xml, application/*+xmlJavaSerializationConverterSerializablex-java-serialization;charset=UTF-8FastJsonHttpMessageConverterObject*/*2. springboot集成RestTemplate根据上述源码的分析学习,可以轻松,简单地在项目进行对RestTemplate进行优雅地使用,比如增加自定义的异常处理、MessageConverter以及拦截器interceptor。本文使用示例demo,详情请查看接下来的内容。2.1. 导入依赖:(RestTemplate集成在Web Start中)<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-web --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.2.0.RELEASE</version> </dependency> <!-- https://mvnrepository.com/artifact/org.projectlombok/lombok --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.10</version> <scope>provided</scope> </dependency> 复制代码2.2. RestTemplat配置:使用ClientHttpRequestFactory属性配置RestTemplat参数,比如ConnectTimeout,ReadTimeout;增加自定义的interceptor拦截器和异常处理;追加message转换器;配置自定义的异常处理.@Configuration public class RestTemplateConfig { @Value("${resttemplate.connection.timeout}") private int restTemplateConnectionTimeout; @Value("${resttemplate.read.timeout}") private int restTemplateReadTimeout; @Bean //@LoadBalanced public RestTemplate restTemplate( ClientHttpRequestFactory simleClientHttpRequestFactory) { RestTemplate restTemplate = new RestTemplate(); //配置自定义的message转换器 List<HttpMessageConverter<?>> messageConverters = restTemplate.getMessageConverters(); messageConverters.add(new CustomMappingJackson2HttpMessageConverter()); restTemplate.setMessageConverters(messageConverters); //配置自定义的interceptor拦截器 List<ClientHttpRequestInterceptor> interceptors=new ArrayList<ClientHttpRequestInterceptor>(); interceptors.add(new HeadClientHttpRequestInterceptor()); interceptors.add(new TrackLogClientHttpRequestInterceptor()); restTemplate.setInterceptors(interceptors); //配置自定义的异常处理 restTemplate.setErrorHandler(new CustomResponseErrorHandler()); restTemplate.setRequestFactory(simleClientHttpRequestFactory); return restTemplate; } @Bean public ClientHttpRequestFactory simleClientHttpRequestFactory(){ SimpleClientHttpRequestFactory reqFactory= new SimpleClientHttpRequestFactory(); reqFactory.setConnectTimeout(restTemplateConnectionTimeout); reqFactory.setReadTimeout(restTemplateReadTimeout); return reqFactory; } } 复制代码2.3. 组件(自定义异常处理、interceptor拦截器、message转化器)自定义interceptor拦截器,实现ClientHttpRequestInterceptor接口自定义TrackLogClientHttpRequestInterceptor,记录resttemplate的request和response信息,可进行追踪分析;自定义HeadClientHttpRequestInterceptor,设置请求头的参数。API发送各种请求,很多请求都需要用到相似或者相同的Http Header。如果在每次请求之前都把Header填入HttpEntity/RequestEntity,这样的代码会显得十分冗余,可以在拦截器统一设置。TrackLogClientHttpRequestInterceptor:/** * @Auther: ccww * @Date: 2019/10/25 22:48,记录resttemplate访问信息 * @Description: 记录resttemplate访问信息 */ @Slf4j public class TrackLogClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException { trackRequest(request,body); ClientHttpResponse httpResponse = execution.execute(request, body); trackResponse(httpResponse); return httpResponse; } private void trackResponse(ClientHttpResponse httpResponse)throws IOException { log.info("============================response begin=========================================="); log.info("Status code : {}", httpResponse.getStatusCode()); log.info("Status text : {}", httpResponse.getStatusText()); log.info("Headers : {}", httpResponse.getHeaders()); log.info("=======================response end================================================="); } private void trackRequest(HttpRequest request, byte[] body)throws UnsupportedEncodingException { log.info("======= request begin ========"); log.info("uri : {}", request.getURI()); log.info("method : {}", request.getMethod()); log.info("headers : {}", request.getHeaders()); log.info("request body : {}", new String(body, "UTF-8")); log.info("======= request end ========"); } } 复制代码HeadClientHttpRequestInterceptor:@Slf4j public class HeadClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { public ClientHttpResponse intercept(HttpRequest httpRequest, byte[] bytes, ClientHttpRequestExecution clientHttpRequestExecution) throws IOException { log.info("#####head handle########"); HttpHeaders headers = httpRequest.getHeaders(); headers.add("Accept", "application/json"); headers.add("Accept-Encoding", "gzip"); headers.add("Content-Encoding", "UTF-8"); headers.add("Content-Type", "application/json; charset=UTF-8"); ClientHttpResponse response = clientHttpRequestExecution.execute(httpRequest, bytes); HttpHeaders headersResponse = response.getHeaders(); headersResponse.add("Accept", "application/json"); return response; } } 复制代码自定义异常处理,可继承DefaultResponseErrorHandler或者实现ResponseErrorHandler接口:实现自定义ErrorHandler的思路是根据响应消息体进行相应的异常处理策略,对于其他异常情况由父类DefaultResponseErrorHandler来进行处理。自定义CustomResponseErrorHandler进行30x异常处理CustomResponseErrorHandler:/** * @Auther: Ccww * @Date: 2019/10/28 17:00 * @Description: 30X的异常处理 */ @Slf4j public class CustomResponseErrorHandler extends DefaultResponseErrorHandler { @Override public boolean hasError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = response.getStatusCode(); if(statusCode.is3xxRedirection()){ return true; } return super.hasError(response); } @Override public void handleError(ClientHttpResponse response) throws IOException { HttpStatus statusCode = response.getStatusCode(); if(statusCode.is3xxRedirection()){ log.info("########30X错误,需要重定向!##########"); return; } super.handleError(response); } } 复制代码自定义message转化器/** * @Auther: Ccww * @Date: 2019/10/29 21:15 * @Description: 将Content-Type:"text/html"转换为Map类型格式 */ public class CustomMappingJackson2HttpMessageConverter extends MappingJackson2HttpMessageConverter { public CustomMappingJackson2HttpMessageConverter() { List<MediaType> mediaTypes = new ArrayList<MediaType>(); mediaTypes.add(MediaType.TEXT_PLAIN); mediaTypes.add(MediaType.TEXT_HTML); //加入text/html类型的支持 setSupportedMediaTypes(mediaTypes);// tag6 } } 复制代码各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
在面试中遇到美女面试官时,我们以为面试会比较容易过,也能好好表现自己技术的时候了。然而却出现以下这一幕,当美女面试官听说你使用过Redis时,那么问题来了。👩面试官:Q1,你知道Redis设置key过期时间的命令吗?👧你:你毫不犹豫的巴拉巴拉说了一堆命令,以及用法,比如expire 等等命令(🎈这时候你想问得那么简单?但真的那么简单吗?美女面试官停顿了一下,接着问)👩面试官:Q2,那你说说Redis是怎么实现过期时间设置呢?以及怎么判断键过期的呢?👧你:(这时候想这还难不倒我),然后又巴拉巴拉的说一通,Redis的数据库服务器中redisDb数据结构以及过期时间的判定(🎈你又在想应该不会问了吧,换个Redis的话题了吧,那你就错了)👩面试官:(抬头笑着看了看你)Q3,那你说说过期键的删除策略以及Redis过期键的删除策略以及实现?🤦️你:这时你回答的就不那么流畅了,有时头脑还阻塞了。(🎈这是你可能就有点蒙了,或者只知道一些过期键的删除策略,但具体怎么实现不知道呀,你以为面试官的提问这样就完了吗?)👩面试官:Q4,那你再说说其他环节中是怎么处理过期键的呢(比如AOF、RDB)?🤦你:...........(🎈这更加尴尬了,知道的不全,也可能不知道,本来想好好表现,也想着面试比较简单,没想到会经历这些)为了避免这尴尬的场景出现,那现在需要你记录下以下的内容,这样就可以在美女面试官面前好好表现了。1. Redis Expire Key基础redis数据库在数据库服务器中使用了redisDb数据结构,结构如下:typedef struct redisDb { dict *dict; /* 键空间 key space */ dict *expires; /* 过期字典 */ dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */ dict *ready_keys; /* Blocked keys that received a PUSH */ dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */ struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */ int id; /* Database ID */ long long avg_ttl; /* Average TTL, just for stats */ } redisDb; 复制代码其中,键空间(key space):dict字典用来保存数据库中的所有键值对过期字典(expires):保存数据库中所有键的过期时间,过期时间用UNIX时间戳表示,且值为long long整数1.1 设置过期时间命令EXPIRE \<key> \<ttl>:命令用于将键key的过期时间设置为ttl秒之后PEXPIRE \<key> \<ttl>:命令用于将键key的过期时间设置为ttl毫秒之后EXPIREAT \<key> \<timesramp>:命令用于将key的过期时间设置为timrestamp所指定的秒数时间戳PEXPIREAT \<key> \<timesramp>:命令用于将key的过期时间设置为timrestamp所指定的毫秒数时间戳设置过期时间:redis> set Ccww 5 2 0 ok redis> expire Ccww 5 ok 复制代码使用redisDb结构存储数据图表示:1.2过期时间保存以及判定过期键的判定,其实通过过期字典进行判定,步骤:检查给定键是否存在于过期字典,如果存在,取出键的过期时间通过判断当前UNIX时间戳是否大于键的过期时间,是的话,键已过期,相反则键未过期。2. 过期键删除策略2.1 三种不同删除策略定时删除:在设置键的过期时间的同时,创建一个定时任务,当键达到过期时间时,立即执行对键的删除操作惰性删除:放任键过期不管,但在每次从键空间获取键时,都检查取得的键是否过期,如果过期的话,就删除该键,如果没有过期,就返回该键定期删除:每隔一点时间,程序就对数据库进行一次检查,删除里面的过期键,至于要删除多少过期键,以及要检查多少个数据库,则由算法决定。2.2 三种删除策略的优缺点2.2.1 定时删除优点: 对内存友好,定时删除策略可以保证过期键会尽可能快地被删除,并释放国期间所占用的内存缺点: 对cpu时间不友好,在过期键比较多时,删除任务会占用很大一部分cpu时间,在内存不紧张但cpu时间紧张的情况下,将cpu时间用在删除和当前任务无关的过期键上,影响服务器的响应时间和吞吐量2.2.2 惰性删除优点: 对cpu时间友好,在每次从键空间获取键时进行过期键检查并是否删除,删除目标也仅限当前处理的键,这个策略不会在其他无关的删除任务上花费任何cpu时间。缺点: 对内存不友好,过期键过期也可能不会被删除,导致所占的内存也不会释放。甚至可能会出现内存泄露的现象,当存在很多过期键,而这些过期键又没有被访问到,这会可能导致它们会一直保存在内存中,造成内存泄露。2.2.4 定期删除由于定时删除会占用太多cpu时间,影响服务器的响应时间和吞吐量以及惰性删除浪费太多内存,有内存泄露的危险,所以出现一种整合和折中这两种策略的定期删除策略。定期删除策略每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对CPU时间的影响。定时删除策略有效地减少了因为过期键带来的内存浪费。定时删除策略难点就是确定删除操作执行的时长和频率:删除操作执行得太频繁。或者执行时间太长,定期删除策略就会退化成为定时删除策略,以至于将cpu时间过多地消耗在删除过期键上。相反,则惰性删除策略一样,出现浪费内存的情况。 所以使用定期删除策略,需要根据服务器的情况合理地设置删除操作的执行时长和执行频率。3. Redis的过期键删除策略Redis服务器结合惰性删除和定期删除两种策略一起使用,通过这两种策略之间的配合使用,使得服务器可以在合理使用CPU时间和浪费内存空间取得平衡点。3.1 惰性删除策略的实现Redis在执行任何读写命令时都会先找到这个key,惰性删除就作为一个切入点放在查找key之前,如果key过期了就删除这个key。robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; expireIfNeeded(db,key); // 切入点 val = lookupKey(db,key); if (val == NULL) server.stat_keyspace_misses++; else server.stat_keyspace_hits++; return val; } 复制代码通过expireIfNeeded函数对输入键进行检查是否删除int expireIfNeeded(redisDb *db, robj *key) { /* 取出键的过期时间 */ mstime_t when = getExpire(db,key); mstime_t now; /* 没有过期时间返回0*/ if (when < 0) return 0; /* No expire for this key */ /* 服务器loading时*/ if (server.loading) return 0; /* 根据一定规则获取当前时间*/ now = server.lua_caller ? server.lua_time_start : mstime(); /* 如果当前的是从(Slave)服务器 * 0 认为key为无效 * 1 if we think the key is expired at this time. * */ if (server.masterhost != NULL) return now > when; /* key未过期,返回 0 */ if (now <= when) return 0; /* 删除键 */ server.stat_expiredkeys++; propagateExpire(db,key,server.lazyfree_lazy_expire); notifyKeyspaceEvent(NOTIFY_EXPIRED, "expired",key,db->id); return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) : dbSyncDelete(db,key); } 复制代码3.2 定期删除策略的实现key的定期删除会在Redis的周期性执行任务(serverCron,默认每100ms执行一次)中进行,而且是发生Redis的master节点,因为slave节点会通过主节点的DEL命令同步过来达到删除key的目的。for (j = 0; j < dbs_per_call; j++) { int expired; redisDb *db = server.db+(current_db % server.dbnum); current_db++; /* 超过25%的key已过期,则继续. */ do { unsigned long num, slots; long long now, ttl_sum; int ttl_samples; /* 如果该db没有设置过期key,则继续看下个db*/ if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; break; } slots = dictSlots(db->expires); now = mstime(); /*但少于1%时,需要调整字典大小*/ if (num && slots > DICT_HT_INITIAL_SIZE && (num*100/slots < 1)) break; expired = 0; ttl_sum = 0; ttl_samples = 0; if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP) num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;// 20 while (num--) { dictEntry *de; long long ttl; if ((de = dictGetRandomKey(db->expires)) == NULL) break; ttl = dictGetSignedIntegerVal(de)-now; if (activeExpireCycleTryExpire(db,de,now)) expired++; if (ttl > 0) { /* We want the average TTL of keys yet not expired. */ ttl_sum += ttl; ttl_samples++; } } /* Update the average TTL stats for this database. */ if (ttl_samples) { long long avg_ttl = ttl_sum/ttl_samples; /样本获取移动平均值 */ if (db->avg_ttl == 0) db->avg_ttl = avg_ttl; db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50); } iteration++; if ((iteration & 0xf) == 0) { /* 每迭代16次检查一次 */ long long elapsed = ustime()-start; latencyAddSampleIfNeeded("expire-cycle",elapsed/1000); if (elapsed > timelimit) timelimit_exit = 1; } /* 超过时间限制则退出*/ if (timelimit_exit) return; /* 在当前db中,如果少于25%的key过期,则停止继续删除过期key */ } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4); } 复制代码依次遍历每个db(默认配置数是16),针对每个db,每次循环随机选择20个(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)key判断是否过期,如果一轮所选的key少于25%过期,则终止迭次,此外在迭代过程中如果超过了一定的时间限制则终止过期删除这一过程。4. AOF、RDB和复制功能对过期键的处理4.1 RDB生成RDB文件程序会数据库中的键进行检查,已过期的键不会保存到新创建的RDB文件中载入RDB文件主服务载入RDB文件,会对文件中保存的键进行检查会忽略过期键加载未过期键从服务器载入RDB文件,会加载文件所保存的所有键(过期和未过期的),但从主服务器同步数据同时会清空从服务器的数据库。4.2 AOFAOF文件写入:当过期键被删除后,会在AOF文件增加一条DEL命令,来显式地记录该键已被删除。AOF重写:已过期的键不会保存到重写的AOF文件中4.3 复制当服务器运行在复制模式下时,从服务器的过期键删除动作由主服务器控制的,这样的好处主要为了保持主从服务器数据一致性:主服务器在删除一个过期键之后,会显式地向所有的从服务器发送一个DEL命令,告知从服务器删除这个过期键从服务器在执行客户端发送的读取命令时,即使碰到过期键也不会将过期键删除,不作任何处理。只有接收到主服务器 DEL命令后,从服务器进行删除处理。各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
MongoDB是基于分布式文件存储的数据库,由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案,且MongodDB是一个介于关系数据库与非关系数据库之间的产品,是非关系型数据库中功能最丰富,最像关系数据库。由于MongoDB的特性以及功能,使得其在企业使用频率很大,所以很多面试都会MongoDB的相关知识,基于网上以及自己阅读官网文档总结2019-2020年MongoDB的面试题。具体如下:1Q:MongoDB的优势有哪些?面向集合(Collection)和文档(document)的存储,以JSON格式的文档保存数据。高性能,支持Document中嵌入Document减少了数据库系统上的I/O操作以及具有完整的索引支持,支持快速查询高效的传统存储方式:支持二进制数据及大型对象高可用性,数据复制集,MongoDB 数据库支持服务器之间的数据复制来提供自动故障转移(automatic failover)高可扩展性,分片(sharding)将数据分布在多个数据中心,MongoDB支持基于分片键创建数据区域.丰富的查询功能, 聚合管道(Aggregation Pipeline)、全文搜索(Text Search)以及地理空间查询(Geospatial Queries)支持多个存储引擎,WiredTiger存储引、In-Memory存储引擎2Q:MongoDB 支持哪些数据类型?java类似数据类型:类型解析String字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的Integer整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位Double双精度浮点值。用于存储浮点值Boolean布尔值。用于存储布尔值(真/假)Arrays用于将数组或列表或多个值存储为一个键Datetime记录文档修改或添加的具体时间MongoDB特有数据类型:类型解析ObjectId用于存储文档 id,ObjectId是基于分布式主键的实现MongoDB分片也可继续使用Min/Max Keys将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比Code用于在文档中存储 JavaScript代码Regular Expression用于在文档中存储正则表达式Binary Data二进制数据。用于存储二进制数据Null用于创建空值Object用于内嵌文档3Q:什么是集合Collection、文档Document,以及与关系型数据库术语类比。集合Collection位于单独的一个数据库MongoDB 文档Document集合,它类似关系型数据库(RDBMS)中的表Table。一个集合Collection内的多个文档Document可以有多个不同的字段。通常情况下,集合Collection中的文档Document有着相同含义。文档Document由key-value构成。文档Document是动态模式,这说明同一集合里的文档不需要有相同的字段和结构。类似于关系型数据库中table中的每一条记录。与关系型数据库术语类比mongodb关系型数据库DatabaseDatabaseCollectionTableDocumentRecord/RowFiledColumnEmbedded DocumentsTable join4Q:什么是”Mongod“,以及MongoDB命令。mongod是处理MongoDB系统的主要进程。它处理数据请求,管理数据存储,和执行后台管理操作。当我们运行mongod命令意味着正在启动MongoDB进程,并且在后台运行。MongoDB命令:命令说明use database_name切换数据库db.myCollection.find().pretty()格式化打印结果db.getCollection(collectionName).find()修改Collection名称5Q:"Mongod"默认参数有?传递数据库存储路径,默认是"/data/db"端口号 默认是 "27017"6Q:MySQL和mongodb的区别形式MongoDBMySQL数据库模型非关系型关系型存储方式虚拟内存+持久化查询语句独特的MongoDB查询方式传统SQL语句架构特点副本集以及分片常见单点、M-S、MHA、MMM等架构方式数据处理方式基于内存,将热数据存在物理内存中,从而达到高速读写不同的引擎拥有自己的特点使用场景事件的记录,内容管理或者博客平台等数据大且非结构化数据的场景适用于数据量少且很多结构化数据7Q:问mongodb和redis区别以及选择原因形式MongoDBredis内存管理机制MongoDB 数据存在内存,由 linux系统 mmap 实现,当内存不够时,只将热点数据放入内存,其他数据存在磁盘Redis 数据全部存在内存,定期写入磁盘,当内存不够时,可以选择指定的 LRU 算法删除数据支持的数据结构MongoDB 数据结构比较单一,但是支持丰富的数据表达,索引Redis 支持的数据结构丰富,包括hash、set、list等性能mongodb依赖内存,TPS较高Redis依赖内存,TPS非常高。性能上Redis优于MongoDB可靠性支持持久化以及复制集增加可靠性Redis依赖快照进行持久化;AOF增强可靠性;增强可靠性的同时,影响访问性能数据分析mongodb内置数据分析功能(mapreduce)Redis不支持事务支持情况只支持单文档事务,需要复杂事务支持的场景暂时不适合Redis 事务支持比较弱,只能保证事务中的每个操作连续执行集群MongoDB 集群技术比较成熟Redis从3.0开始支持集群 选择原因:架构简单没有复杂的连接深度查询能力,MongoDB支持动态查询。容易调试容易扩展不需要转化/映射应用对象到数据库对象使用内部内存作为存储工作区,以便更快的存取数据。8Q:如何执行事务/加锁?mongodb没有使用传统的锁或者复杂的带回滚的事务,因为它设计的宗旨是轻量,快速以及可预计的高性能.可以把它类比成mysql mylsam的自动提交模式.通过精简对事务的支持,性能得到了提升,特别是在一个可能会穿过多个服务器的系统里.9Q:更新操作会立刻fsync到磁盘?不会,磁盘写操作默认是延迟执行的.写操作可能在两三秒(默认在60秒内)后到达磁盘,通过 syncPeriodSecs 启动参数,可以进行配置.例如,如果一秒内数据库收到一千个对一个对象递增的操作,仅刷新磁盘一次.MongoDB索引10Q: 索引类型有哪些?单字段索引(Single Field Indexes)复合索引(Compound Indexes)多键索引(Multikey Indexes)全文索引(text Indexes)Hash 索引(Hash Indexes)通配符索引(Wildcard Index)2dsphere索引(2dsphere Indexes)11Q:MongoDB在A:{B,C}上建立索引,查询A:{B,C}和A:{C,B}都会使用索引吗?由于MongoDB索引使用B-tree树原理,只会在A:{B,C}上使用索引MongoDB索引详情可看文章【MongoDB系列--轻松应对面试中遇到的MongonDB索引(index)问题】,其中包括很多索引的问题:创建索引,需要考虑的问题索引限制问题索引类型详细解析索引的种类问题12Q:什么是聚合聚合操作能够处理数据记录并返回计算结果。聚合操作能将多个文档中的值组合起来,对成组数据执行各种操作,返回单一的结果。它相当于 SQL 中的 count(*) 组合 group by。对于 MongoDB 中的聚合操作,应该使用aggregate()方法。详情可查看文章【MongoDB系列--深入理解MongoDB聚合(Aggregation)】,其中包括很多聚合的问题:聚合管道(aggregation pipeline)的问题Aggregation Pipeline 优化等问题Map-Reduce函数的问题
github:github.com/Ccww-lx/Spr…模块:spring-boot-base-mongodb在NoSQL盛行的时代,App很大可能会涉及到MongoDB数据库的使用,而也必须学会在Spring boot使用Spring Data连接MongoDB进行数据增删改查操作,如下为详细的操作手册。1. 依赖直接导入spring-data-mongodb包或者使用Spring Boot starter<dependencies> <!-- other dependency elements omitted --> <dependency> <groupId>org.springframework.data</groupId> <artifactId>spring-data-mongodb</artifactId> <version>2.2.0.RELEASE</version> </dependency> </dependencies> <!--spring 框架使用最新的 --> <spring.framework.version>5.2.0.RELEASE</spring.framework.version> <!--用一即可--> <!--使用Spring Boot starter--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> 复制代码2. 属性文件application.properties#mongodb连接地址,集群用“;”隔开 spring.mongo.mongoDatabaseAddress=10.110.112.165:27092;10.110.112.166:27092 #mongo数据名 spring.mongo.dbname=mongodb #mongo用户 spring.mongo.username=mongodbopr #mongo密码 spring.mongo.password=123456 #mongo最大连接数 spring.mongo.connectionsPerHost=50 复制代码3. mongodb 配置注册Mongo实例配置:@Configuration public class MongodbConfig { public static final String COMMA = ";"; public static final String COLON = ":"; @Value("${spring.mongo.mongoDatabaseAddress}") private String mongoDatabaseAddress; @Value("${spring.mongo.username}") private String username; @Value("${spring.mongo.dbname}") private String dbname; @Value("${spring.mongo.password}") private String password; @Value("${spring.mongo.connectionsPerHost}") private String connectionsPerHost; /** * 获取mongodb的地址 * * @return */ private List<ServerAddress> getMongoDbAddress() { List<ServerAddress> serverAddrList = new ArrayList<ServerAddress>(); //如果有多个服务器的话 if (this.mongoDatabaseAddress.indexOf(COMMA) > 0) { String[] addressArrays = mongoDatabaseAddress.split(COMMA); String[] hostPort; for (String address : addressArrays) { hostPort = address.split(COLON); ServerAddress serverAdress = new ServerAddress(hostPort[0], Integer.valueOf(hostPort[1])); serverAddrList.add(serverAdress); } } else { String[] hostPort = mongoDatabaseAddress.split(COLON); ServerAddress serverAdress = new ServerAddress(hostPort[0], Integer.valueOf(hostPort[1])); serverAddrList.add(serverAdress); } return serverAddrList; } /** * 设置连接参数 */ private MongoClientOptions getMongoClientOptions() { MongoClientOptions.Builder builder = MongoClientOptions.builder(); // todo 添加其他参数配置 //最大连接数 builder.connectionsPerHost(Integer.valueOf(connectionsPerHost)); MongoClientOptions options = builder.readPreference(ReadPreference.nearest()).build(); return options; } /** * * @return */ @Bean public MongoClient mongoClient() { //使用数据库名、用户名密码登录 MongoCredential credential = MongoCredential.createCredential(username, dbname, password.toCharArray()); //创建Mongo客户端 return new MongoClient(getMongoDbAddress(), credential, getMongoClientOptions()); } /** * 注册mongodb操作类 * @param mongoClient * @return */ @Bean @ConditionalOnClass(MongoClient.class) public MongoTemplate mongoTemplate(MongoClient mongoClient) { MongoTemplate mongoTemplate = new MongoTemplate(new SimpleMongoDbFactory(mongoClient, dbname)); return mongoTemplate; } } 复制代码4. mongodb操作使用MongoTemplate类进行增删改查@Service public class MongodbService { @Autowired private MongoTemplate mongoTemplate; /** * 新增文档 * * @param userDTO * @return */ public UserDTO insert(UserDTO userDTO) { //insert方法并不提供级联类的保存,所以级联类需要先自己先保存 return mongoTemplate.insert(userDTO); } public UserDTO save(UserDTO userDTO) { Sort sort = new Sort(Sort.Direction.DESC, "name"); userDTO = mongoTemplate.findOne(Query.query(Criteria.where("")).with(sort), UserDTO.class); return mongoTemplate.save(userDTO); } /** * 删除文档 * NOTE:remove方法不支持级联删除所以要单独删除子数据 * @param name */ public void remove(String name) { //根据名字查询数据并删除 UserDTO userDTO = mongoTemplate.findOne(Query.query(Criteria.where("name").is(name)), UserDTO.class); //remove方法不支持级联删除所以要单独删除子数据 List<AddressDTO> addressList = userDTO.getAddressList(); for (AddressDTO addressDTO : addressList) { mongoTemplate.remove(addressDTO); } //删除主数据 mongoTemplate.remove(userDTO); } /** * 更新文档 * @param userDTO */ public void update(UserDTO userDTO) { mongoTemplate.updateFirst(Query.query(Criteria.where("name").is(userDTO.getName())), Update.update("age", userDTO.getAge()), UserDTO.class); } /** * 查询文档 * @param name */ public void find(String name) { Sort sort = new Sort(Sort.Direction.DESC, "name"); List<UserDTO> userDTOS = mongoTemplate.find(Query.query(Criteria.where("name").is(name)), UserDTO.class); //基于sort排序使用findOne查询最新一条记录 UserDTO userDTO = mongoTemplate.findOne(Query.query(Criteria.where("name").is(name)).with(sort), UserDTO.class); //模糊查询 List<UserDTO> userDTOList = mongoTemplate.find(Query.query(Criteria.where("name").is(name).regex(name)).with(sort), UserDTO.class); //分页查询 Pageable pageable = PageRequest.of(3, 20, sort); List<UserDTO> userDTOPageableList = mongoTemplate.find(Query.query(Criteria.where("name").is(name)).with(pageable), UserDTO.class); //总数 long conut = mongoTemplate.count(Query.query(Criteria.where("name").is(name)), UserDTO.class); Page<UserDTO> page = new PageImpl(userDTOPageableList, pageable, conut); } } 复制代码NOTE:在开发中,如果从任何MongoDB操作返回的com.mongodb.WriteResult包含错误,则可以方便地记录或引发异常。 通常,在开发过程中很容易忘记执行此操作,然后最终得到一个看似运行成功的App,但实际上该数据库操作发生异常,没执行成功。 可以将MongoTemplate的WriteResultChecking属性设置为以下值之一:EXCEPTION:引发ExceptionNONE:不执行任何操作,默认值对于更高级的情况,可以将每个操作设置不同的WriteConcern值(用于删除,更新,插入和保存操作),则可以在MongoTemplate上配置WriteConcernResolver的策略接口。 由于MongoTemplate用于持久化POJO,因此WriteConcernResolver允许您创建一个策略,该策略可以将特定的POJO类映射到WriteConcern值。WriteConcernResolver接口:public interface WriteConcernResolver { WriteConcern resolve(MongoAction action); } 复制代码自定义WriteConcernResolver接口,实现不同WriteConcern策略:private class MyAppWriteConcernResolver implements WriteConcernResolver { public WriteConcern resolve(MongoAction action) { if (action.getEntityClass().getSimpleName().contains("UserDTO")) { return WriteConcern.NONE; } else if (action.getEntityClass().getSimpleName().contains("Metadata")) { return WriteConcern.JOURNAL_SAFE; } return action.getDefaultWriteConcern(); } } 复制代码5. 常用的类以及方法解析5.1 MongoClient、ServerAddress、MongoCredential以及MongoClientOptions基于ServerAddress单机或者Replica Set在使用MongoClient连接mongodb数据库注册mongo实例,在注册示例中可能要使得MongoCredential账号密码验证以及使用MongoClientOptions配置mongodb其他的参数。MongoClient常用的构造器方法:public MongoClient(String host){} public MongoClient(MongoClientURI uri){} public MongoClient(String host, MongoClientOptions options) {} public MongoClient(ServerAddress addr, MongoCredential credential, MongoClientOptions options){} public MongoClient(List<ServerAddress> seeds, MongoCredential credential, MongoClientOptions options){} 复制代码5.2 MongoTemplate使用MongoTemplate结合Sort、Criteria、Query、Update以及分页Pageable类灵活地进行对mongodb数据库进行增删改查。query方法://根据查询条件查询 public <T> List<T> find(Query query, Class<T> entityClass){} //根据查询条件查询返回一条记录 public <T> <T>findOne(Query query, Class<T> entityClass){} //查询该collection所有记录 public <T> List<T> findAll(Class<T> entityClass){} 复制代码insert方法://新增一条记录 public <T> T insert(T objectToSave){} //在collectionName中新增一条记录 public <T> T insert(T objectToSave, String collectionName) {} // public <T> T save(T objectToSave){} 复制代码remove方法://根据Object删除 public DeleteResult remove(Object object) {} //根据查询条件进行删除 public DeleteResult remove(Query query, Class<?> entityClass){} 复制代码update方法:// public UpdateResult upsert(Query query, Update update, Class<?> entityClass) {} //更新查询出来的第一条记录 public UpdateResult updateFirst(Query query, Update update, String collectionName) {} 复制代码5.3 SortSort查询排序类。Sort类常用方法://构造方法创建一个排序。direction为排序方向的枚举类型,properties为排序字段数组 Sort(Sort.Direction direction, String... properties) //多个排序条件链接 and(Sort sort) //返回升序排列对象 ascending() //返回降序排列对象 descending() 复制代码5.4 CriteriaCriteria查询条件类,类似于SQL的where,常用方法://声明定义查询条件,且为静态方法 where(String key) //与操作 and(String key) //正则表达式,即可为模糊查询 regex(String re) //包含 in(Object... o) //大于 gt(Object o) //大于等于 gte(Object o) //等于 is(Object o) //小于 lt(Object o) //小于等于 lte(Object o) //非 not() //创建与操作 andOperator(Criteria... criteria) 复制代码5.5 QueryQuery查询对象,具有查询的全部信息,其中包括筛选条件、排序、返回数量等。常用的方法://定义查询对象,静态方法 query(CriteriaDefinition criteriaDefinition) //在本次查询添加一个CriteriaDefinition查询条件 addCriteria(CriteriaDefinition criteriaDefinition) //添加一个Sort排序对象 with(Sort sort) //添加一个Pageable分页对象、通常情况下,分页和排序一起使用。 with(Pageable pageable) 复制代码详细接口信息可查看【MogoDB API官方文档】6.常用注解注解解析@Id用于标记id字段,没有标记此字段的实体也会自动生成id字段,但是我们无法通过实体来获取id。id建议使用ObjectId类型来创建@Document用于标记此实体类是mongodb集合映射类@DBRef用于指定与其他集合的级联关系,但是需要注意的是并不会自动创建级联集合@Indexed用于标记为某一字段创建索引@CompoundIndex用于创建复合索引@TextIndexed:用于标记为某一字段创建全文索引@Language指定documen语言@Transient:被该注解标注的,将不会被录入到数据库中。只作为普通的javaBean属性@Field:用于指定某一个字段映射到数据库中的名称各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
9. 数据库连接配置MySQL连接配置:(1) 引入jar包依赖环境<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency>(2)属性文件配置#连接驱动 #spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver #连接url spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true #账号 spring.datasource.username=root #密码 spring.datasource.password=rootOracle连接配置:(1) 引入jar包依赖环境<!-- https://mvnrepository.com/artifact/com.oracle/ojdbc6 --> <dependency> <groupId>com.oracle</groupId> <artifactId>ojdbc6</artifactId> <version>11.2.0.3</version> </dependency>(2)属性文件配置#oracle配置 spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver spring.datasource.url=jdbc:oracle:thin:@127.0.0.1:1521:test spring.datasource.username=root spring.datasource.password=123456SQL Server连接配置:(1) 引入jar包依赖环境<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <scope>runtime</scope> </dependency>(2)属性文件配置#SQLServer配置 spring.datasource.driverClassName=com.microsoft.sqlserver.jdbc.SQLServerDriver spring.datasource.url=jdbc:sqlserver://127.0.0.1:1433;DatabaseName=test spring.datasource.username=sa spring.datasource.password=123456PostgreSQL连接配置(1) 引入jar包依赖环境<!-- postgresql驱动 --> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> </dependency>(2)属性文件配置#PostgreSQL配置 spring.datasource.driverClassName=org.postgresql.Driver spring.datasource.url=jdbc:postgresql://127.0.0.1:5432/test spring.datasource.username=postgres spring.datasource.password=123456H2数据源连接配置:(1) 引入jar包依赖环境<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>(2)属性文件配置#连接驱动 spring.datasource.driver-class-name=org.h2.Driver #数据表结构信息 spring.datasource.schema=classpath:db/schema-h2.sql #数据表数据 spring.datasource.data=classpath:db/data-h2.sql #连接url spring.datasource.url=jdbc:h2:mem:test #账户密码 spring.datasource.username=root spring.datasource.password=testMongoDB连接配置(1)引入jar包依赖环境!-- spring-boot-starter-data-mongodb --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency>(2)属性文件配置:MongoDB 2.4以下版本:# 主机地址 spring.data.mongodb.host=127.0.0.1 # 端口 spring.data.mongodb.port=27017 # 账号密码 spring.data.mongodb.username=root spring.data.mongodb.password=root # 数据库 spring.data.mongodb.database=testMongoDB 2.4以上版本:spring.data.mongodb.uri=mongodb://root(userName):root(password)@localhost(ip地址):27017(端口号)/test(collections/数据库)示例:spring.data.mongodb.uri=mongodb://root:root@127.0.0.1:27017/test10. 发送邮件配置(1)引入jar包依赖环境<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>com.sun.mail</groupId> <artifactId>javax.mail</artifactId> <version>RELEASE</version> </dependency>(2) 属性文件配置#163邮箱 spring.mail.host=smtp.163.com spring.mail.username=xxx@163.com spring.mail.password=xxx #qq邮箱 #spring.mail.host=smtp.qq.com #spring.mail.username=xxx@qq.com #spring.mail.password=xxx spring.mail.default-encoding=UTF-8 #其他邮箱配置类似11. 常用Redis配置(1) 引入jar包依赖环境<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>(2)application.properties属性文件配置# REDIS # Redis数据库索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=localhost # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) 默认 8 spring.redis.lettuce.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1 spring.redis.lettuce.pool.max-wait=-1 # 连接池中的最大空闲连接 默认 8 spring.redis.lettuce.pool.max-idle=8 # 连接池中的最小空闲连接 默认 0 spring.redis.lettuce.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=100012. 配置Druid数据源(1) 引入jar依赖环境<!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.17</version> </dependency> <!-- https://mvnrepository.com/artifact/log4j/log4j --> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.17</version> </dependency>(2) application.properties属性文件配置这里以MySQL为例,如下:# 驱动配置信息 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=root # 连接池的配置信息 # 初始化大小,最小,最大 spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 # 配置获取连接等待超时的时间 spring.datasource.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 FROM DUAL spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false # 打开PSCache,并且指定每个连接上PSCache的大小 spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 spring.datasource.filters=stat,wall,log4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000(3) 代码配置import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.support.http.StatViewServlet; import com.alibaba.druid.support.http.WebStatFilter; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.web.servlet.FilterRegistrationBean; import org.springframework.boot.web.servlet.ServletRegistrationBean; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; import java.util.HashMap; import java.util.Map; /** * @ClassName: DruidConfiguration * @Author: liuhefei * @Description: Druid数据库配置 * @Date: 2019/6/6 19:57 */ @Configuration public class DruidConfiguration { private static final Logger log = LoggerFactory.getLogger(DruidConfiguration.class); @Bean public ServletRegistrationBean druidServlet() { log.info("init Druid Servlet Configuration "); ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(); servletRegistrationBean.setServlet(new StatViewServlet()); servletRegistrationBean.addUrlMappings("/druid/*"); Map<String, String> initParameters = new HashMap<>(); initParameters.put("loginUsername", "admin");// 用户名 initParameters.put("loginPassword", "admin");// 密码 initParameters.put("resetEnable", "false");// 禁用HTML页面上的“Reset All”功能 initParameters.put("allow", ""); // IP白名单 (没有配置或者为空,则允许所有访问) //initParameters.put("deny", "192.168.20.1");// IP黑名单 (存在共同时,deny优先于allow) servletRegistrationBean.setInitParameters(initParameters); return servletRegistrationBean; } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } @ConfigurationProperties(prefix = "spring.datasource") //属性前缀 @Bean public DataSource druid(){ return new DruidDataSource(); } }13. 配置Swagger2接口文档(1) 引入jar依赖环境<!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger2 --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger2</artifactId> <version>2.9.2</version> </dependency> <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui --> <dependency> <groupId>io.springfox</groupId> <artifactId>springfox-swagger-ui</artifactId> <version>2.9.2</version> </dependency>(2) 代码配置import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; import springfox.documentation.builders.ApiInfoBuilder; import springfox.documentation.builders.PathSelectors; import springfox.documentation.builders.RequestHandlerSelectors; import springfox.documentation.service.ApiInfo; import springfox.documentation.service.Contact; import springfox.documentation.spi.DocumentationType; import springfox.documentation.spring.web.plugins.Docket; import springfox.documentation.swagger2.annotations.EnableSwagger2; /** * @ClassName: SwaggerConfig * @Desc: swagger文档配置 * @Author: liuhefei * @Date: 2019/6/11 10:29 */ @Configuration @EnableSwagger2 @Profile(value = {"dev", "test"}) //指定运行环境(开发,测试) public class SwaggerConfig { @Bean public Docket createRestApi() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("你项目的包路径")) //此处必须修改 .paths(PathSelectors.any()) .build(); } private ApiInfo apiInfo() { return new ApiInfoBuilder().title("Api接口接口") .description("api接口描述信息") .contact(new Contact("作者", "url", "email")) .termsOfServiceUrl("https://swagger.io/swagger-ui/") .version("1.0") .build(); } }14. 配置项目的名称:Spring.application.name=项目名称15. Http编码相关的配置spring.http.encoding.charset=UTF-8 spring.http.encoding.enabled=true spring.http.encoding.force=true16. JPA相关的配置spring.jpa.database=mysql spring.jpa.show-sql=true spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5Dialect spring.jpa.database=sql_server spring.jpa.show-sql=true spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.SQLServer2012Dialect spring.jpa.hibernate.ddl-auto=none17. logback日志的配置(1)在pom.xml文件中引入依赖<!-- https://mvnrepository.com/artifact/ch.qos.logback/logback-classic --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> <scope>test</scope> </dependency>(2)在项目的resources目录下引入logback-spring.xml配置文件即可,内容如下:方式一:<?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- 日志根目录--> <springProperty scope="context" name="LOG_HOME" source="logging.path" defaultValue="/logs/spring-boot-logback"/> <!-- 日志级别 --> <springProperty scope="context" name="LOG_ROOT_LEVEL" source="logging.level.root" defaultValue="DEBUG"/> <!-- 标识这个"STDOUT" 将会添加到这个logger --> <springProperty scope="context" name="STDOUT" source="log.stdout" defaultValue="STDOUT"/> <!-- 日志文件名称--> <property name="LOG_PREFIX" value="spring-boot-logback" /> <!-- 日志文件编码--> <property name="LOG_CHARSET" value="UTF-8" /> <!-- 日志文件路径+日期--> <property name="LOG_DIR" value="${LOG_HOME}/%d{yyyyMMdd}" /> <!--对日志进行格式化--> <property name="LOG_MSG" value="- | [%X{requestUUID}] | [%d{yyyyMMdd HH:mm:ss.SSS}] | [%level] | [${HOSTNAME}] | [%thread] | [%logger{36}] | --> %msg|%n "/> <!--文件大小,默认10MB--> <property name="MAX_FILE_SIZE" value="50MB" /> <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志--> <property name="MAX_HISTORY" value="10"/> <!--输出到控制台--> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <!-- 输出的日志内容格式化--> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${LOG_MSG}</pattern> </layout> </appender> <!--输出到文件--> <appender name="0" class="ch.qos.logback.core.rolling.RollingFileAppender"> </appender> <!-- 定义 ALL 日志的输出方式:--> <appender name="FILE_ALL" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!--日志文件路径,日志文件名称--> <File>${LOG_HOME}/all_${LOG_PREFIX}.log</File> <!-- 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件路径,新的 ALL 日志文件名称,“ i ” 是个变量 --> <FileNamePattern>${LOG_DIR}/all_${LOG_PREFIX}%i.log</FileNamePattern> <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志--> <MaxHistory>${MAX_HISTORY}</MaxHistory> <!--当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB--> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!-- 输出的日志内容格式化--> <layout class="ch.qos.logback.classic.PatternLayout"> <pattern>${LOG_MSG}</pattern> </layout> </appender> <!-- 定义 ERROR 日志的输出方式:--> <appender name="FILE_ERROR" class="ch.qos.logback.core.rolling.RollingFileAppender"> <!-- 下面为配置只输出error级别的日志 --> <filter class="ch.qos.logback.classic.filter.LevelFilter"> <level>ERROR</level> <OnMismatch>DENY</OnMismatch> <OnMatch>ACCEPT</OnMatch> </filter> <!--日志文件路径,日志文件名称--> <File>${LOG_HOME}/err_${LOG_PREFIX}.log</File> <!-- 设置滚动策略,当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB --> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <!--日志文件路径,新的 ERR 日志文件名称,“ i ” 是个变量 --> <FileNamePattern>${LOG_DIR}/err_${LOG_PREFIX}%i.log</FileNamePattern> <!-- 配置日志的滚动时间 ,表示只保留最近 10 天的日志--> <MaxHistory>${MAX_HISTORY}</MaxHistory> <!--当天的日志大小超过 ${MAX_FILE_SIZE} 文件大小时候,新的内容写入新的文件, 默认10MB--> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>${MAX_FILE_SIZE}</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <!-- 输出的日志内容格式化--> <layout class="ch.qos.logback.classic.PatternLayout"> <Pattern>${LOG_MSG}</Pattern> </layout> </appender> <!-- 配置以配置包下的所有类的日志的打印,级别是 ERROR--> <logger name="com.lhf.springboot" level="ERROR" /> <!-- ${LOG_ROOT_LEVEL} 日志级别 --> <root level="${LOG_ROOT_LEVEL}"> <!-- 标识这个"${STDOUT}"将会添加到这个logger --> <appender-ref ref="${STDOUT}"/> <!-- FILE_ALL 日志输出添加到 logger --> <appender-ref ref="FILE_ALL"/> <!-- FILE_ERROR 日志输出添加到 logger --> <appender-ref ref="FILE_ERROR"/> </root> </configuration>方式二:<?xml version="1.0" encoding="UTF-8"?> <configuration> <!--定义日志文件的存储地址 勿在 LogBack的配置中使用相对路径 --> <property name="LOG_HOME" value="/tmp/log" /> <!-- 控制台输出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> </appender> <!-- 按照每天生成日志文件 --> <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <FileNamePattern>${LOG_HOME}/logs/smsismp.log.%d{yyyy-MM-dd}.log</FileNamePattern> <!--日志文件保留天数 --> <MaxHistory>30</MaxHistory> </rollingPolicy> <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder"> <!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 --> <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{30} - %msg%n</pattern> </encoder> <!--日志文件最大的大小 --> <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy"> <MaxFileSize>10MB</MaxFileSize> </triggeringPolicy> </appender> <!-- 日志输出级别 --> <root level="INFO"> <appender-ref ref="STDOUT" /> <appender-ref ref="FILE" /> </root> <!-- 定义各个包的详细路径,继承root的值 --> <logger name="com.lhf.springboot" level="INFO" /> <!-- 此值由 application.properties的spring.profiles.active=dev指定--> <springProfile name="dev"> <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 --> <property name="LOG_HOME" value="/tmp/log" /> <logger name="com.lhf.springboot" level="DEBUG" /> </springProfile> <springProfile name="prod"> <!--定义日志文件的存储地址 勿在 LogBack 的配置中使用相对路径 --> <property name="LOG_HOME" value="/home" /> <logger name="com.lhf.springboot" level="INFO" /> </springProfile> </configuration>18. log4j2日志配置(1)在pom.xml文件中引入依赖jar包,如下:<dependency> <!-- 引入log4j2依赖 --> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-log4j2</artifactId> </dependency>或者<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-core --> <dependency> <groupId>org.apache.logging.log4j</groupId> <artifactId>log4j-core</artifactId> <version>2.12.0</version> </dependency>(2)在项目的resources目录下引入log4j2.xml配置文件,文件内容如下:<?xml version="1.0" encoding="UTF-8"?> <!--Configuration后面的status,这个用于设置log4j2自身内部的信息输出,可以不设置,当设置成trace时,你会看到log4j2内部各种详细输出--> <!--monitorInterval:Log4j能够自动检测修改配置 文件和重新配置本身,设置间隔秒数--> <configuration monitorInterval="5"> <!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL --> <!--变量配置--> <Properties> <!-- 格式化输出:%date表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度 %msg:日志消息,%n是换行符--> <!-- %logger{36} 表示 Logger 名字最长36个字符 --> <property name="LOG_PATTERN" value="%date{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n" /> <!-- 定义日志存储的路径,不要配置相对路径 <property name="FILE_PATH" value="更换为你的日志路径" /> <property name="FILE_NAME" value="更换为你的项目名" /> --> <property name="FILE_PATH" value="E:\code\log" /> <property name="FILE_NAME" value="spring-boot-log4j2" /> </Properties> <appenders> <console name="Console" target="SYSTEM_OUT"> <!--输出日志的格式--> <PatternLayout pattern="${LOG_PATTERN}"/> <!--控制台只输出level及其以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> </console> <!--文件会打印出所有信息,这个log每次运行程序会自动清空,由append属性决定,适合临时测试用--> <File name="Filelog" fileName="${FILE_PATH}/test.log" append="false"> <PatternLayout pattern="${LOG_PATTERN}"/> </File> <!-- 这个会打印出所有的info及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileInfo" fileName="${FILE_PATH}/info.log" filePattern="${FILE_PATH}/${FILE_NAME}-INFO-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="info" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 这个会打印出所有的warn及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileWarn" fileName="${FILE_PATH}/warn.log" filePattern="${FILE_PATH}/${FILE_NAME}-WARN-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="warn" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> <!-- 这个会打印出所有的error及以下级别的信息,每次大小超过size,则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩,作为存档--> <RollingFile name="RollingFileError" fileName="${FILE_PATH}/error.log" filePattern="${FILE_PATH}/${FILE_NAME}-ERROR-%d{yyyy-MM-dd}_%i.log.gz"> <!--控制台只输出level及以上级别的信息(onMatch),其他的直接拒绝(onMismatch)--> <ThresholdFilter level="error" onMatch="ACCEPT" onMismatch="DENY"/> <PatternLayout pattern="${LOG_PATTERN}"/> <Policies> <!--interval属性用来指定多久滚动一次,默认是1 hour--> <TimeBasedTriggeringPolicy interval="1"/> <SizeBasedTriggeringPolicy size="10MB"/> </Policies> <!-- DefaultRolloverStrategy属性如不设置,则默认为最多同一文件夹下7个文件开始覆盖--> <DefaultRolloverStrategy max="15"/> </RollingFile> </appenders> <!--Logger节点用来单独指定日志的形式,比如要为指定包下的class指定不同的日志级别等。--> <!--然后定义loggers,只有定义了logger并引入的appender,appender才会生效--> <loggers> <!--过滤掉spring和mybatis的一些无用的DEBUG信息--> <logger name="org.mybatis" level="info" additivity="false"> <AppenderRef ref="Console"/> </logger> <!--监控系统信息--> <!--若是additivity设为false,则 子Logger 只会在自己的appender里输出,而不会在 父Logger 的appender里输出。--> <Logger name="org.springframework" level="info" additivity="false"> <AppenderRef ref="Console"/> </Logger> <root level="info"> <appender-ref ref="Console"/> <appender-ref ref="Filelog"/> <appender-ref ref="RollingFileInfo"/> <appender-ref ref="RollingFileWarn"/> <appender-ref ref="RollingFileError"/> </root> </loggers> </configuration>19. 在属性文件中设置上传文件的大小限制# 设置上传文件的大小限制 spring.servlet.multipart.max-file-size=100MB spring.servlet.multipart.max-request-size=100MB20. 设置开启Debug模式application.debug.enabled=true21. 设置访问URL前缀server.servlet.context-path=/xxl-job-admin22. 几种数据源配置类型(以MySQL为例)1. tomcat-jdbc引入依赖jar包:<dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-jdbc</artifactId> <version>9.0.24</version> </dependency>属性文件配置:spring.datasource.url=jdbc:mysql://127.0.0.1:3306/test?Unicode=true&characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.type=org.apache.tomcat.jdbc.pool.DataSource spring.datasource.tomcat.max-wait=10000 spring.datasource.tomcat.max-active=30 spring.datasource.tomcat.test-on-borrow=true spring.datasource.tomcat.validation-query=SELECT 1 spring.datasource.tomcat.validation-interval=300002. spring-boot-starter-jdbc引入依赖jar包<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency>属性文件配置:spring.datasource.type=com.zaxxer.hikari.HikariDataSource spring.datasource.driver-class-name=com.mysql.jdbc.Driver spring.datasource.hikari.username=root spring.datasource.hikari.password=root spring.datasource.hikari.jdbc-url=jdbc:mysql://127.0.0.1:3306/test?autoReconnect=true&characterEncoding=UTF-8&allowMultiQueries=true&useSSL=false spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.maximum-pool-size=15 spring.datasource.hikari.auto-commit=true spring.datasource.hikari.idle-timeout=30002 spring.datasource.hikari.pool-name=DatebookHikariCP spring.datasource.hikari.max-lifetime=500000 spring.datasource.hikari.connection-timeout=30001 spring.datasource.hikari.connection-test-query=SELECT 1 spring.datasource.hikari.validation-timeout=50003. druid引入依赖jar包:<dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.20</version> </dependency>属性文件配置:# 驱动配置信息 spring.datasource.type=com.alibaba.druid.pool.DruidDataSource spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://127.0.0.1:3306/lhf_springboot1?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&useSSL=true spring.datasource.username=root spring.datasource.password=root # 连接池的配置信息 # 初始化大小,最小,最大 spring.datasource.initialSize=5 spring.datasource.minIdle=5 spring.datasource.maxActive=20 # 配置获取连接等待超时的时间 spring.datasource.maxWait=60000 # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 spring.datasource.timeBetweenEvictionRunsMillis=60000 # 配置一个连接在池中最小生存的时间,单位是毫秒 spring.datasource.minEvictableIdleTimeMillis=300000 spring.datasource.validationQuery=SELECT 1 FROM DUAL spring.datasource.testWhileIdle=true spring.datasource.testOnBorrow=false spring.datasource.testOnReturn=false # 打开PSCache,并且指定每个连接上PSCache的大小 spring.datasource.poolPreparedStatements=true spring.datasource.maxPoolPreparedStatementPerConnectionSize=20 # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙 spring.datasource.filters=stat,wall,log4j # 通过connectProperties属性来打开mergeSql功能;慢SQL记录 spring.datasource.connectionProperties=druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
以application.properties属性文件为例:1. 在pom.xml中配置Java版本:<plugin> <groupid>org.apache.maven.plugins</groupid> <artifactid>maven-compiler-plugin</artifactid> <version>3.6</version> <configuration> <source>1.8</source> <target>1.8</target> </configuration> </plugin>2. 在pom.xml中设置项目编码:<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties>3. 在属性文件中配置Tomcat#启动端口 server.port=8080 #当项目出错时跳转的页面 server.error.path=/error #session失效时间,30m表示30分钟 server.servlet.session.timeout=30m #项目路径名称,默认为/ server.servlet.context-path=/ #配置tomcat请求编码 server.tomcat.uri-encoding=utf-8 #Tomcat最大线程数 server.tomcat.max-threads=500 #存放Tomcat运行日志和临时文件的目录,若不配值则默认使用系统的临时目录 server.tomcat.basedir=/home/tmp4. 在属性文件中配置HTTPS利用Java数字证书管理工具keytool生成一个数字证书,cmd命令窗口生成命令:keytool -genkey -alias tomcathttps -keyalg RSA -keysize 2048 -keystore key.p12 -validity 365命令解释:• -genkey 表示要创建一个新的密钥。• -alias 表示 keystore 的别名。• -keyalg 表示使用的加密算法是 RSA, 一种非对称加密算法.• -keysize 表示密钥的长度.• -keystore 表示生成的密钥存放位直。• -validity 表示密钥的有效时间,单位为天。将生成的数字证书添加到项目根目录下,在application.properties中做如下配置:#秘钥文件名 server.ssl.key- store=key.pl2 #秘钥别名,就是在cmd命令执行中alias的参数 server.ssl.key-alias=tomcathttps #秘钥密码,就是在cmd命令执行中输入的密码 server.ssl.key- store-password=l23456在浏览器中访问项目时,记得添加信任即可访问。5. 在pom.xml中配置Jetty服务器<dependency> <groupid>org. springframework.boot</groupid> <artifactid>spring-boot-starter-web</artifactid> <!--禁用tomcat服务器--> <exclusions> <exclusion> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-tomcat </artifactid> </exclusion> </exclusions> </dependency> <dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-jetty</artifactid> </dependency>6. 切换启动环境配置#test:测试环境/dev:开发环境/prod:生产环境 spring.profiles.active=test/dev/prod7.整合Thymeleaf模板属性配置在pom.xml中引入thymeleaf依赖,如下:<dependency> <groupid>org.springframework.boot</groupid> <artifactid>spring-boot-starter-thymeleaf</artifactid> </dependency>常见的属性配置:#是否开启缓存,开发时可设置为 false,默认为 true spring.thymeleaf.cache=true #检查模板是否存在,默认为 true spring.thymeleaf.check-template=true #检查模板位置是否存在,默认为 true spring.thymeleaf.check-template-location=true #模板文件编码 spring.thymeleaf.encoding=UTF-8 #模板文件位置 spring.thymeleaf.prefix=classpath:/templates/ #Content-Type 配置 spring.thymeleaf.servlet.content-type=text/html #模板文件后缀 spring.thymeleaf.suffix=.html8. 整合Freemarker模板属性配置在pom.xml中引入freemarker依赖配置,如下:<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-freemarker</artifactId> </dependency>常见的属性配置:#HttpServletRequest 的属性是否可以覆盖 controller 中 model 的同名项 spring.freemarker.allow-request-override=false #HttpSession 的属性是否可以覆盖 controller 中 model 的同名项 spring.freemarker.allow-session-override=false #是否开启缓存 spring.freemarker.cache=false #模板文件编码 spring.freemarker.charset=UTF-8 #是否检查模板位置 spring.freemarker.check-template-location=true #Content-Type 的值 spring.freemarker.content-type=text/html #是否将 HttpServletRequest 中的属性添加到 Model 中 spring.freemarker.expose-request-attributes=false #是否将 HttpSession 中的属性添加到 Model 中 spring.freemarker.expose-session-attributes=false #模板文件后缀 spring.freemarker.suffix=.ftl #模板文件位置 spring.freemarker.template-loader-path=classpath: /templates/ #设定静态文件路径,js,css等 spring.mvc.static-path-pattern=/static/**
安全问题其实是很多程序员容易忽略的问题但需要我们重视起来,提高应用程序的安全性。常出现的安全问题包括,程序接受数据可能来源于未经验证的用户,网络连接和其他不受信任的来源,如果未对程序接受数据进行校验,则可能会引发安全问题等等,具体也可以分成以下几方面:数据校验敏感信息加密算法序列化与反序列化I/O操作多线程安全框架和组件在文章安全开发规范:开发人员必须了解开发安全规范(一)(涉及安全问题,以及解决方法和代码实现)中我们阐述了一些关于数据检验的安全问题,接下来我们继续其他部分的安全性问题分析与解决。数据校验-权限管理规则1.10:禁止程序数据进行增、删、改、查实对客户端请求的数据过分相信而遗漏对于权限的判定垂直越权漏洞: 称为权限提升,是一种“基于URL的访问控制”设计缺陷引起的漏洞。由于Web应用程序没有做权限控制或者仅在菜单上做了权限控制,导致恶意用户只要猜测其他管理页面的URL,就可以访问或控制其他角色拥有的数据或页面,达到权限提升的目的。水平越权漏洞: 一种“基于数据的访问控制”设计缺陷引起的漏洞。由于服务器端在接收到请求数据进行操作时没有判断数据的所属人而导致的越权数据访问漏洞。如服务器端从客户端提交的request参数(用户能够控制的数据)中获取用户id,恶意攻击者通过变换请求ID的值,查看或修改不属于本人的数据。反例:@RequestMapping(value = "delete") public String delete(HttpServletRequest request, @RequestParam long id) throws Exception{ try { userManage.delete(id); request.setAttribute("msg","delete user success"); }catch (Exception e){ request.setAttribute("msg","delete user failure"); } return list(request); } @RequestMapping(value = "/delete/{addrId}") public Object remove(@RequestParam long addrId) { Map<String,Object> resMap=new HashMap<>(); if(WebUtils.isLogged){ this.addressService.removeUserAddress(addrId); resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_SUCCESS); resMap.put(Constans.MESSAGE,"remove user address success"); }else { resMap.put(Constans.RESP_STATUS_CODE_KEY,Constans.RESP_STATUS_CODE_FAIL); resMap.put(Constans.MESSAGE,"user is not login ,remove user address failure"); } return resMap; } 复制代码正例:垂直越权漏洞:在调用功能之前,验证当前用户身份是否有权限调用相关功能(推荐使用过滤器,进行统一权限验证)public void doPost(HttpServletRequest request, HttpServletResponse response)throws ServletException ,IOException{ if(request.getSession(true).getAttribute("manager")==null){ response.sendRedirect("noright.html"); return; } UserManagerService userManagerService=new UserManagerService(); request.setCharacterEncoding("utf-8"); response.setCharacterEncoding("utf-8"); String action=request.getParameter("action"); if("add".equals(action)){ String id=request.getParameter("userId"); String name=request.getParameter("userName"); String sex=request.getParameter("userSex"); } //todo do somethings } 复制代码数据校验-权限管理通过全局过滤器来检测用户是否登录,是否对资源具有访问权限。权限访问规则存入数据库中web.xml中配置过滤器public class PriviegeFilter implements Filter{ @Autowired private UserManagerService userManagerService;@Override public void init(FilterConfig filterConfig) throws ServletException { List<UserAuthorization> userAuthorizationS=userManagerService.getUserAuthorizationInfo(); } @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { for(UserAuthorization userAuthorization: userAuthorizationS){ // 从数据库中获取用户授权信息 if(!authen){ throw new RuntimeException("您无权访问页面,请以合适身份登陆后查看"); } filterChain.doFilter(servletRequest,servletResponse); } } @Override public void destroy() { } } 复制代码数据校验-权限管理SpringMVC:Spring Security提供了“基于URL的访问控制”和“基于Method的访问控制”。在用户进行操作时,从session中获取用户id,将传入的参数与用户的身份做绑定校验。<sec:http> <sec:intercept-url parttern="/persident_portal/*" access="RILE_PERSIDENT"/> <sec:intercept-url parttern="/manager_portal/*" access="RILE_MANAGER"/> <sec:intercept-url parttern="/**" access="RILE_USER"/> <sec:form-login /> <sec:logout /> </sec:http> 复制代码数据校验-不安全的网络传输http协议属于明文传输协议,交互过程以及数据传输都没有进行加密,通信双方也没有进行任何认证,通信过程非常容易遭遇劫持、监听、篡改,严重情况下,会造成恶意的流量劫持等问题,甚至造成个人隐私泄露(比如银行卡卡号和密码泄露)等严重的安全问题。HTTPS在HTTP的基础上加入了SSL协议,SSL依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。****对称加密非对称加密https协议(http+ssl协议),如下图所示为其连接过程:中间人攻击数字证书:解决中间人攻击数字证书是一个经证书授权中心数字签名的包含公开密钥拥有者信息以及公开密钥的文件 客户端拿到证书后,根据证书用第三方的私钥进行上的方法自己生成一个证书编号,如果自己生成的证书编号与证书上的证书编号相同,那 么说明这个证书是真实的。同时,为避免证书编号本身又被调包,所以使加密。总结HTTPS要使客户端与服务器端的通信过程得到安全保证,必须使用的对称加密算法,但是协商对称加密算法的过程,需要使用非对称加密算法 来保证安全,然而直接使用非对称加密的过程本身也不安全,会有中间人篡改公钥的可能性,所以客户端与服务器不直接使用公钥,而是使用 数字证书签发机构(CA)颁发的证书来保证非对称加密过程本身的安全,为了保证证书不被篡改,引入数字签名,客户端使用相同的对称加 密算法,来验证证书的真实性,如此,最终解决了客户端与服务器端之间的通信安全问题。数据校验-不安全的网络传输规则1.11:敏感数据在跨信任域之间传递采用签名加密传输敏感数据传输过程中要防止窃取和恶意篡改。使用安全的加密算法加密传输对象可以保护数据。这就是所谓的对对象进行密封。而对密封的对象进行数字签名则可以防止对象被非法篡改,保持其完整性public static void main(String[] args) throwsIOException,ClassNotFoundException{ // Build map SerializableMap<String, Integer> map = buildMap(); // Serialize map ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data")); out.writeObject(map); out.close(); // Deserialize map ObjectInputStream in = new ObjectInputStream(new FileInputStream("data")); map = (SerializableMap<String, Integer>) in.readObject(); in.close(); // Inspect map InspectMap(map); } 复制代码反例:public static void main(String[] args) throwsIOException,GeneralSecurityException, ClassNotFoundException{ // Build map SerializableMap<String, Integer> map = buildMap(); // Generate sealing key & seal map KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(newSecureRandom()); Key key= generator.generateKey(); Cipher cipher= Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, key); SealedObject sealedMap= new SealedObject(map, cipher); // Serialize map ObjectOutputStreamout = new ObjectOutputStream(newFileOutputStream("data")); out.writeObject(sealedMap); out.close(); // Deserialize map ObjectInputStream in = newObjectInputStream(newFileInputStream("data")); sealedMap= (SealedObject) in.readObject(); in.close(); // Unseal map cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher); // Inspect map InspectMap(map); } public static void main(String[] args) throwsIOException, GeneralSecurityException, ClassNotFoundException{ SerializableMap<String, Integer> map = buildMap(); KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(newSecureRandom()); Key key= generator.generateKey(); Cipher cipher= Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, key); SealedObjectsealedMap= newSealedObject(map, cipher); KeyPairGeneratorkpg= KeyPairGenerator.getInstance("RSA"); KeyPair kp= kpg.generateKeyPair(); Signature sig = Signature.getInstance("SHA256withRSA"); SignedObject signedMap= newSignedObject(sealedMap, kp.getPrivate(), sig); ObjectOutputStreamout = newObjectOutputStream(newFileOutputStream("data")); out.writeObject(signedMap); out.close(); ObjectInputStream in = newObjectInputStream(newFileInputStream("data")); signedMap= (SignedObject) in.readObject(); in.close(); if(!signedMap.verify(kp.getPublic(), sig)){ throw new GeneralSecurityException("Map failed verification"); } sealedMap= (SealedObject) signedMap.getObject(); cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); map = (SerializableMap<String, Integer>) sealedMap.getObject(cipher); InspectMap(map); } 复制代码正例:public static void main(String[] args) throws IOException, GeneralSecurityException, ClassNotFoundException{ SerializableMap<String, Integer> map = buildMap(); KeyPairGenerator kpg= KeyPairGenerator.getInstance("RSA"); KeyPair kp= kpg.generateKeyPair(); Signature sig = Signature.getInstance("SHA256withRSA"); SignedObject signedMap= new SignedObject(map, kp.getPrivate(), sig); KeyGenerator generator = KeyGenerator.getInstance("AES"); generator.init(new SecureRandom()); Key key= generator.generateKey(); Cipher cipher= Cipher.getInstance("AES"); cipher.init(Cipher.ENCRYPT_MODE, key); SealedObject sealedMap = new SealedObject(signedMap, cipher); ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("data")); out.writeObject(sealedMap); out.close(); // Deserialize map ObjectInputStream in = new ObjectInputStream(new FileInputStream("data")); sealedMap= (SealedObject) in.readObject(); in.close(); // Unseal map cipher = Cipher.getInstance("AES"); cipher.init(Cipher.DECRYPT_MODE, key); signedMap= (SignedObject) sealedMap.getObject(cipher); // Verify signature and retrieve map if(!signedMap.verify(kp.getPublic(), sig)){ throw new GeneralSecurityException("Map failed verification"); } map = (SerializableMap<String, Integer>) signedMap.getObject(); // Inspect map InspectMap(map); } 复制代码敏感信息敏感信息-常见的敏感信息规则2.1:禁止在日志中明文保存用户敏感数据日志中如明文保存用户敏感数据,容易泄露给运维人员或者攻破系统的攻击者规则2.2:禁止将敏感信息硬编码在程序中如果将敏感信息(包括口令和加密密钥)硬编码在程序中,可能会将敏感信息暴露给攻击者。任何能够访问到class 文件的人都可以反编译class文件并发现这些敏感信息... DriverManager.getConnection(url,"soctt","tiger") ... 复制代码Java反编译javap c Connmngr.classldc #36://String jdbc:mysql://ixne.com/rxsqlldc #38://String scottldc #17://String tiger反例:public class IPaddress{ private String ipAddress= "172.16.254.1"; public static voidmain(String[] args){ //... } } 复制代码正例:public class IPaddress{ public static void main(String[] args) throws IOException{ char[] ipAddress= new char[100]; BufferedReader br= new BufferedReader(newInputStreamReader(newFileInputStream("serveripaddress.txt"))); // Reads the server IP address into the char array, // returns the number of bytes read intn = br.read(ipAddress); // Validate server IP address // Manually clear out the server IP address // immediately after use for(inti= n -1; i>= 0; i--){ ipAddress[i] = 0; } br.close(); } } 复制代码规则2.3:加密传输邮件-邮件传输时需使用安全协议SSL/TLS加密传输,避免攻击者在网络上嗅探到用户数据反例:邮件传输时未使用TLS协议public class SendMailTLS{ public static void main(String[] args) { final String username="username@gmail.com"; final String password="password"; Properties props=new Properties(); //使用TLS //props.put("mail.smtp.auth","true"); //props.put("mail.smtp.startls.enable","true"); //使用SSL //props.put("mail.smtp.socketFactory,port","465"); //props.put("mail.smtp.socketFactory.class","javax.net.ssl.SSLSocketFactory"); //props.put("mail.smtp.auth","true"); props.put("mail.smtp.host","smtp.gmail.com"); props.put("mail.smtp.port","587"); Session session=Session.getInstance(props,new javax.mail.Authenticator){ protected PasswordAuthentication getPasswordAuthentication(){ return new PasswordAuthentication(username,password); } }); } } 复制代码正例:加密使用TLS协议public class SendMailTLS{ public static void main(String[] args) { final String username="username@gmail.com"; final String password="password"; Properties props=new Properties(); //使用TLS props.put("mail.smtp.auth","true"); props.put("mail.smtp.startls.enable","true"); //做服务器证书校验 props.put("mail.smtp.ssl.checkserveridentity","true"); //添加信任的服务器地址,多个地址之间用空格分开 props.put("mail.smtp.ssl.trust","smtp.gmail.com"); props.put("mail.smtp.host","smtp.gmail.com"); props.put("mail.smtp.port","25"); Session session=Session.getInstance(props,new javax.mail.Authenticator){ protected PasswordAuthentication getPasswordAuthentication(){ return new PasswordAuthentication(username,password); } }); } } 复制代码规则2.4:基于hash算法的口令存储必须加盐值(salt),并且使用标准的迭代PBKDF2如果不加盐值,会导致相同的密码得到相同的Hash值public class PasswordHash{ public static final String PBKDF2_ALGORITHM="PBKDF2WithHmacSHA1"; public static final int SALT_BYTE_SIZE=24; public static final int HASH_BYTE_SIZE=24; public static final int PBKDF2_ITERATIONS=1000; public static String createHash(char[] password) throws NoSuchAlgorithmException,InvalidKeySpecException{ //Generate a random salt SecureRandom random =new SecureRandom(); byte[] salt =new btye[SALT_BYTE_SIZE]; random.nextBytes(salt); // Hash the password byte[] hash =pbkdf2(password,salt,PBKDF2_ITERATIONS,HASH_BYTE_SIZE); //format iterations:salt:hash return PBKDF2_ITERATIONS+":"+toHex(hash); } } 复制代码加密算法加密算法-加密算法总览规则3.1不安全加密算法-禁止使用不安全的加密算法DES\3DES加密算法应该使用安全的加密算法AES攻击者能够破解不安全的加密算法,获取到敏感信息反例:使用了不安全算法DESbyte[] result = DES.encrypt(str.getBytes(),password); //直接将如上内容解密 try{ byte[] decryResult =DES.decrypt(result,password); System.out.println("解密后:"+new String(decryResult)); }catch(Excption e1){ e1.printStackTrace(); } 复制代码规则3.3:对称加密算法AES-禁止使用AES中不安全的分组模式ECB,推荐使用不仅提供加密并且还提供完整性校验的AES-GCMAES分组密码模式还有:AES-CBC\AES-CFB\AES-OFB\AES-CTR(AES-CTR由于能并行计算,效率最高)反例:使用了AES中不安全的分组模式ECBpubilc class AES{ //加密 public static String Encrypt(String sSrc,String sKey) throws Excetion{ if(sKey==null){ System.out.print("key为空null"); return null; } //判断key是否为16位 if(key.length()!=16){ System.out.print("key长度不是16位"); return null; } byte[] raw =sKey.getBytes("UTF-8"); SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES"); Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//算法/模式/补码方式 cipher.init(Cipher.ENCRYPT_MODE,skeySpec); byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8")); return new Base64().encodeToString(encrypted);//此处使用Base64做转码功能,同时能起到2次加密作用 } } 复制代码正例:使用AES算法中安全的分组模式CBCpubilc class AES{ //加密 public static String Encrypt(String sSrc,String sKey) throws Excetion{ if(sKey==null){ System.out.print("key为空null"); return null; } //判断key是否为16位 if(key.length()!=16){ System.out.print("key长度不是16位"); return null; } byte[] raw =sKey.getBytes("UTF-8"); SecreKeySpec skeySpec=new SecreKeySpec(raw,"AES"); Cipher cipher=Cipher.getInstance("AES/ECB/PKCS5Padding");//算法/模式/补码方式 IvParameterSpec iv =new IvParameterSpec(sKey.getByte());//使用CBC模式,需要一个向量iv //可增加加密算法的强度 cipher.init(Cipher.ENCRYPT_MODE,skeySpec,iv); byte[] encrypted= cipher.doFinal(sScr.getByte("utf-8")); return new Base64().encodeToString(encrypted);//此处使用Base64做转码功能,同时能起到2次加密作用 } } 复制代码规则3.4:非对称加密算法RSA-非对称加密算法RSA的使用需要注意长度至少为2048位RSA的密钥长度如果低于2048位,达不到安全标准反例:RSA算法的密钥长度只有1024位public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{ HashMap<String,Object> map=new HashMap<String,Object>; KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA"); keyPairGen.initialize(1024); KeyPair keyPair=keyPairGen.generateKeyPair(); RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate(); map.put("public",pubilcKey); map.put("private",privateKey); return map; } 复制代码正例:RSA算法的密钥长度至少2048位public static HashMap<String,Object>getKeys()throws NoSuchAlgorithmException{ HashMap<String,Object> map=new HashMap<String,Object>; KeyPairGenerator keyPairGen=KeyPairGenerator.getInstance("RSA"); keyPairGen.initialize(2048); KeyPair keyPair=keyPairGen.generateKeyPair(); RSAPublicKey pubilcKey=(RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate(); map.put("public",pubilcKey); map.put("private",privateKey); return map; } 复制代码规则3.5:敏感数据加密使用强随机数伪随机数生成器具有可移植性和可重复性,攻击者可以在系统的一些安全脆弱点上监听,并构建相应的查询表预测将要使用的seed值,从而去预测相关的敏感数据伪随机数示例:import java.util.Random; public class RandomDemo{ Random random1=new Random(100); System.out.println(random1.nextInt()); System.out.println(random1.nextFloat()); System.out.println(random1.nextBoolean()); Random random2=new Random(100); System.out.println(random2.nextInt()); System.out.println(random2.nextFloat()); System.out.println(random2.nextBoolean()); } import java.io.UnsupporedEncodeingException; import java.util.Random; public class SecureRandom{ public static void main(String[] args)throws UnsupporedEncodeingException{ Random ranGen=new Random(); byte[] aesKey=new byte[20]; ranKey.nextBytes(aesKey); StringBuffer hexString =new StringBuffer(); for (int i=0;i<aesKey.length;i++){ String hex=Integer.toHexString(0xff&aesKey[i]); if(hex.length()==1) hexString.append(''0); hexString.append(hex); } System.out.println(hexString); } } 复制代码强随机数示例:import java.io.UnsupporedEncodeingException; import java.util.Random; import java.security.SecureRandom; public class SecureRandom{ public static void main(String[] args)throws UnsupporedEncodeingException{ Random ranGen=new SecureRandom(); byte[] aesKey=new byte[20]; ranKey.nextBytes(aesKey); StringBuffer hexString =new StringBuffer(); for (int i=0;i<aesKey.length;i++){ String hex=Integer.toHexString(0xff&aesKey[i]); if(hex.length()==1) hexString.append(''0); hexString.append(hex); } System.out.println(hexString); } } 复制代码序列化与反序列化Java 序列化是指把Java 对象转换为字节序列的过程便于保存在内存、文件、数据库中,ObjectOutputStream类的writeObject()方法可以实现序列化。Java 反序列化是指把字节序列恢复为Java 对象的过程,ObjectInputStream 类的readObject() 方法用于反序列化。public class Test{ public static void main(String[] args) throws Exception{ //定义myObj对象 MyObject myObj=new MyObject(); //创建一个包含对象进行反序列化信息的“object”数据文件 FileOutputStream fos=new FileOutputStream("object"); ObjectOutputStream os =new ObjectOutputStream(fos); //writeObject()方法将myObj对象写入objct文件中 os.writeObject(myObj); os.close(); //从文件中反序列化obj对象 FileInputStream fis=new FileInputStream("object"); ObjectInputStream ois =new ObjectInputStream(fis); //恢复对象 MyObject objectFromDisk=(MyObject)ois.readObject(); System.out.println(objectFromDisk.name); ois.close; } } class MyObject implements Serializable{ public String name; //重写readObject()方法 private void readObject(java.io.ObjectInputStream in) throws IOExeption{ //执行默认的readObject()方法 in.defaultReadObject(); //执行打开计算器程序命令 Runtime.getRuntime().exec("open /Application/Calcultor.app") } } 复制代码序列化与反序列化-反序列化漏洞类ObjectInputStream在反序列化时,应对生成的对象的类型做限制反例:不安全的反序列化操作,导致执行任意命令、控制服务器ServerSocket serverSocket =new ServerSocket(Integer,parseInt("9999")); while(true){ Socket socket=serverSocket.accpet(); ObjectInputStream objectInputStream=new ObjectInputStream(socket.getInputStream()); try{ Object object=objectInputStream.readObject(); }catch(Exception e){ e.printStackTrace(); } } 复制代码规则4.1:类ObjectInputStream在反序列化时,对生成的对象的类型做限制public final class SecureObjectInputStram extends ObjectInputStream{ public SecureObjectInputStram() throws IOException{ super(); } public SecureObjectInputStram( InputStream in) throws IOException{ super(in); } portected Class<?>resolveClass(ObjectStreamClass desc) throws ClassNotFoundException,IOException{ if(!desc.getName().equals("java.security.Person")){ throws new ClassNotFoundException(desc.getName()+"not found"); } return super.resolveClass(desc); } } 复制代码序列化与反序列化-不安全的反序列化漏洞解决方案解决方案:使用安全的反序列化插件,对于业界爆出的反序列化漏洞,及时更新插件和打补丁。传输的数据应该加密,并且在后台进行数据校验,保证数据没有被篡改。自定义ObjectInputStream, 重载resolveClass的方法,对className进行白名单校验。Java9可以继承java.io.ObjectInputFilter类重写checkInput方法实现自定义的过滤器。通过扩展SecurityManager 来实现禁止JVM 执行外部命令Runtime.exec。I/O操作规则5.1:文件上传应根据业务的需要限定文件的格式和大小反例:未限定格式@RequestMapping(value="/upload",method=RequestMethod.POST) public String upload(HttpServletRequest request, @RequestParam("description") String description, @RequestParam("file") MultipartFile file)throws Exception{ if(!file.isEmpty()){ String path=request.getServletContext().getRealPath("/images/"); //上传文件名 String filename=file.getOriginalFilename(); File filepath=new File(path,filename); //判断路径是否存在,如果不存在就创建一个 if(!filepath.getParentFile().exists()){ filepath.getParentFile.mkdirs(); } //将文件上传保存到一个目标文件中 file.transferTo(new File(path+File.separator+filename)); return "success" }else{ return "error"; } } 复制代码正例:限定文件的上传的格式@RequestMapping(value="/upload",method=RequestMethod.POST) public String upload(HttpServletRequest request, @RequestParam("description") String description, @RequestParam("file") MultipartFile file)throws Exception{ if(!file.isEmpty()){ String path=request.getServletContext().getRealPath("/images/"); //上传文件名 String filename=file.getOriginalFilename(); //还有一种方式filenameUtils.getExtension(); String suffix=filename.substring(filename.lastIndexOf(".")+1) if(suffix!="jpg"){ File filepath=new File(path,filename); //判断路径是否存在,如果不存在就创建一个 if(!filepath.getParentFile().exists()){ filepath.getParentFile.mkdirs(); } //将文件上传保存到一个目标文件中 file.transferTo(new File(path+File.separator+filename)); } return "success" ; }else{ return "error"; } } 复制代码规则5.2:路径遍历-文件下载的地方,应对文件的路径进行校验,或者使用文件id映射到文件的方式下载文件反例:protected void doGet(HttpServletRequest request,HttpServeltResponse response) throws ServeltExceptio,IOException{ //获取项目部署绝对路径下的upload文件夹路径,下载upload目录下的文件 String root =request.getServeltContext().getRealPath("/upload"); //获取文件名 String filename=request.getParameter("filename"); //根据文件路径创建输入流 File file=new File(root+"/"+filename); FileInputStream fis= new FileInputStream(file); //设置响应头 response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes())); response.addHeader("Content-Length",""+file.length()); byte[] b =new byte[fis.availabe()]; fis.read(b); response.getOutStream().write(b); } 复制代码正例:使用白名单校验文件名protected void doGet(HttpServletRequest request,HttpServeltResponse response) throws ServeltExceptio,IOException{ //获取项目部署绝对路径下的upload文件夹路径,下载upload目录下的文件 String root =request.getServeltContext().getRealPath("/upload"); //获取文件名 String filename=request.getParameter("filename"); if(filename==FILENAME){ //根据文件路径创建输入流 File file=new File(root+"/"+filename); FileInputStream fis= new FileInputStream(file); //设置响应头 response.addHeader("Content-Disposition","attachment;filename="+new String(filename.getBytes())); response.addHeader("Content-Length",""+file.length()); byte[] b =new byte[fis.availabe()]; fis.read(b); response.getOutStream().write(b); } } 复制代码规则5.3:未释放的流资源(Unreleased Resource,比如Streams使用文件、IO流、数据库连接等)主动释放的资源使用文件、IO流、数据库连接等不会自动释放的资源时,未在使用完毕后马上将其关闭,关闭资源的代码应在try...catch...finally{if (fos != null) {fos.close();}}的finally内执行private static void TestCloseFileStream(){ String fileName=""; //声明引用 InputStream inputStream=null; try{ inputStream=new FileInputStream(filename); }catch(IOException e){ //do something }finally{ if(inputStream!=null){ try{ //关闭流 inputStream.close(); }catch(IOException e){ //do something } } } } 复制代码规则5.4:临时文件使用完毕应及时删除反例:public class TempFile{ public static void main(STring[] args) throws IOExcption{ File f =new File("tempnam.tmp"); if(f.exists()){ System.out.println("This file already exists"); return; } FileOutputStream fop=null; try{ fop=new FileOutputStream(f); String str="Data"; fop.write(str.getBytes()); }finally{ if(fop!=null){ try{ fop.close(); }catch(IOException e){ // handle error } } } } } 复制代码正例:public class TempFile{ public static void main(STring[] args) throws IOExcption{ File f =new File("tempnam.tmp"); if(f.exists()){ System.out.println("This file already exists"); return; } FileOutputStream fop=null; try{ fop=new FileOutputStream(f); String str="Data"; fop.write(str.getBytes()); }finally{ if(fop!=null){ try{ fop.close(); }catch(IOException e){ // handle error } //delete file when finished if(!f.delete()){ //log the error } } } } } 复制代码多线程安全(Double-Checked Locking)规则6.1:多线程下采用加锁机制,防止多线程操作产生的死锁双重锁机制(多线程下不安全)public class Singleton{ private static Singleton singleton; private Singleton(){} public static Singleton getSingleton(){ if(singleton==null){ synchronized (Singleton.class){ if(singleton==null){ singleton=new Singleton(); } } } return singleton; } } singleton = new Singleton()非原子性 1. memory=allocate();//1:分配对象的内存空间 2. ctorInstance(memory);//2:初始化对象 3. singleton=memory;//3:设置instance指向刚分配的内存地址 指令重排后: 1. memory=allocate();//1:分配对象的内存空间 2. singleton=memory;//3:设置instance指向刚分配的内存地址//注意,此时对象还没有被初始化! 3. ctorInstance(memory);//2:初始化对象 复制代码多线程安全(Double-Checked Locking)对方法使用synchronized关键字public class Singleton{ private static Singleton singleton; private Singleton(){} public static synchronized Singleton getSingleton(){ if(singleton==null){ singleton=new Singleton(); } return singleton; } } 复制代码提前初始化public class Singleton{ private static class SingletonHolder{ private static final Singleton singleton=new Singleton(); } private Singleton(){} public static final Singleton getSingleton(){ return SingletonHolder.INSTANCE; } } 复制代码框架和组件-框架和组件安全规则7.1:使用安全版本的框架和组件,官网下载,使用框架和组件,先到网上搜扫下是否有公开的漏洞比如: --WebLogic、Struts2、Nginx --Fastjson、ImageMagick、Log4j规则7.2:禁止使用来源不明的框架或者组件规则7.3:及时更新框架和组件版本异常行为-异常信息暴露到外部规则8.1:异常信息禁止暴露到前端try{ FileInputStream fis =new FileInputStream(System.getenv("APPDATA")+args[0]); }catch(FileNotFoundException e){ //Log the exception throws new IOException ("Unable to retrieve file",e); } 复制代码解析:产生固定的错误信息,未泄露异常信息到外部
安全问题其实是很多程序员想了解又容易忽略的问题,但需要我们重视起来,提高应用程序的安全性。常出现的安全问题包括,程序接受数据可能来源于未经验证的用户,网络连接和其他不受信任的来源,如果未对程序接受数据进行校验,则可能会引发安全问题等等,具体也可以分成以下几方面:数据校验敏感信息加密算法序列化与反序列化I/O操作多线程安全框架和组件数据校验数据校验-校验策略1. 白名单策略 -接受已知好的数据( 任何时候,尽可能使用“白名单”的策略 )下面的示例代码确保 name参数只包含字母、以及下划线if (Pattern.matches("^[0 -9A -Za -z_]+$", name)){ throw new IllegalArgumentException("Invalid name"); } 复制代码2. 黑名单策略 -拒绝已知好的数据public String removeJavascript(String input){ Pattern p = Pattern.compile("javascript", Pattern.CASE_INSENSITIVE ); Matcher m = p.matcher(input); return (! m.matches()) ? input : ""; } 复制代码3. 白名单净化对数据中任何不属于某个已验证的、合法字符列表进行删除编码或者替换,然后再使用这些净化的数据4. 黑名单净化: 剔除或者转换某些字符(例如,删除引号、转换成HTML实体)public static String quoteApostrophe(String input){ if (input != null){ return input.replaceAll(" \'","&rsquo;"); } else{ return null; } } 复制代码数据校验 -输入输出规则1.1 校验跨信任边界传递的不可数据**程序接受的不可信数据源跨越任边界传递必须经过内校验,包括输入和出校验。 不可信数据:用户、网络连接等源 不可信数据:用户、网络连接等源 数据入口:终端计算机互联网出入口广域网出入口公司对外发布服务的 DMZ服务器VPN和类似远程连接设备。 信任边界:根据威胁建模划分的信任边 如 web 应用的服务端;规则 1.2:禁止直接使用不可信数据来拼SQL语句SQL 注入是指原始SQL查询被动态更改成一个与程序预期完全不同的查询。执行这样后可能导致信息泄露或者数据被篡改。防止 SQL注入的方式主要可以分为两类:使用参数化查询 (推荐使用)对不可信数据进行校验预编译处理Statement stmt= null; ResultSet rs= null; try{ String userName= ctx.getAuthenticatedUserName(); //this is a constant String sqlString= "SELECT * FROM t_item WHERE owner='" + userName+ "' AND itemName='" + request.getParameter("itemName") + "'"; stmt= connection.createStatement(); rs= stmt.executeQuery(sqlString);// ... result set handling } 添加 name' OR 'a' = 'a SELECT * FROM t_item WHERE owner = 'wiley' AND itemname= 'name' OR 'a'='a'; 复制代码预编译处理:PreparedStatement stmt= null ResultSet rs=null try { String userName= ctx.getAuthenticatedUserName(); //this is a constant String itemName= request.getParameter(""); // ...Ensure that the length of userName and itemNameis legitimate // ... String sqlString= "SELECT * FROM t_item WHERE owner=? AND itemName=?"; stmt= connection.prepareStatement(sqlString); stmt.setString(1, userName); stmt.setString(2, itemName); rs=stmt.executeQuery(); // ... result set handling }catch(SQLExceptions e) { // ... logging and error handling } 复制代码在存储过程中,通拼接参数值来构建查询字符串和应用序代码一样同是有SQL注入风险反例:CallableStatement= null ResultSet results = null; try{ String userName= ctx.getAuthenticatedUserName(); //this is a constant String itemName= request.getParameter("itemName"); cs= connection.prepareCall("{call sp_queryItem(?,?)}"); cs.setString(1, userName); cs.setString(2, itemName); results = cs.executeQuery(); // ... result set handling }catch(SQLException se){ // ... logging and error handling } 复制代码对应的SQL Server存储过程:CREATE PROCEDURE sp_queryItem @userNamevarchar(50), @itemNamevarchar(50) AS BEGIN DECLARE @sql nvarchar(500); SET @sql= 'SELECT * FROM t_item WHERE owner = ''' + @userName+ ''' AND itemName= ''' + @itemName+ ''''; EXEC(@sql); END GO 复制代码正例:** 在存储过程中动态构建sql,采用预编译的方式防御sql注入,**CallableStatement= null ResultSet results = null; try{ String userName= ctx.getAuthenticatedUserName(); //this is a constant String itemName=request.getParameter("itemName"); // ... Ensure that the length of userName and itemName is legitimate // ... cs= ("{call sp_queryItem(?,?)}"); cs.setString(1, userName); cs.setString(2, itemName); results = cs.executeQuery(); // ... result set handling }catch(SQLException se){ // ... logging and error handling } 复制代码对应的SQL Server存储过程:CREATE PROCEDURE sp_queryItem @userName varchar(50), @itemName varchar(50) AS BEGIN SELECT * FROM t_item WHERE userName= @userName AND itemName= @itemName; END 复制代码使用Hibernate,如果在动态构建SQL/HQL查询时包含了不可信输入,同样也会面临SQL/HQL注入的问题。反例://原生sql查询 String userName= ctx.getAuthenticatedUserName(); //this is a constantString itemName= request.getParameter("itemName"); Query sqlQuery= session.createSQLQuery("select * from where owner = '" + userName+ "' and itemName= '" + itemName+ “’”); List<Item> rs= (List<Item>) sqlQuery.list(); //HQL查询 String userName= ctx.getAuthenticatedUserName(); //this is a constant String itemName=request.getParameter("itemName"); Query hqlQuery= session.createQuery("from Item as item where item.owner= '" + userName+ "' and = '" + itemName+ "'"); List<Item> hrs= (List<Item>) hqlQuery.list(); 复制代码正例://HQL中基于位置的参数化查询: String userName= ctx.getAuthenticatedUserName(); String itemName=request.getParameter("itemName"); Query hqlQuery= session.createQuery("from Item as item where item.owner= ? and item.itemName= ?"); hqlQuery.setString(1, userName); hqlQuery.setString(2, itemName); List<Item> rs= (List<Item>) hqlQuery.list(); //HQL中基于名称的参数化查询: String userName= ctx.getAuthenticatedUserName(); String itemName= ("itemName"); Query hqlQuery= session.createQuery("from Item as item where item.owner= :owner and = :itemName"); hqlQuery.setString("owner", userName); hqlQuery.setString("itemName", itemName); List<Item> rs= (List<Item>) hqlQuery.list(); //原生参数化查询: String userName=ctx.getAuthenticatedUserName(); //this is a constant String itemName= request.getParameter("itemName"); Query sqlQuery= session.createSQLQuery("select * from t_itemwhere owner = ? and itemName= ?"); sqlQuery.setString(0, owner); sqlQuery.setString(1, itemName); List<Item> rs= (List<Item>) sqlQuery.list(); 复制代码Mybaits和ibaits的#和$Mybaits:<select id="getItems" parameterClass="MyClass" resultClass="Item"> SELECT * FROM t_item WHERE owner = #userName# AND itemName= #itemName# </select> String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName=?"; PreparedStatement stmt= connection.prepareStatement(sqlString); stmt.setString(1, myClassObj.getUserName()); stmt.setString(2, myClassObj.getItemName()); ResultSet rs= stmt.executeQuery(); // ... convert results set to Item objects 复制代码ibaits:<select id="getItems" parameterClass="MyClass"="items"> SELECT * FROM t_item WHERE owner = #userName# AND itemName= '$itemName$' </select> String sqlString= "SELECT * FROM t_itemWHERE owner=? AND itemName='" +myClassObj.getItemName() + "'"; PreparedStatementstmt=connection.prepareStatement(sqlString); stmt.setString(1, myClassObj.getUserName()); ResultSetrs= stmt.executeQuery(); 复制代码输入验证,针对无法参数化查询的场景public List<Book> queryBooks(queryCondition){ try{ StringBuilder sb= StringBuilder("select * from t_bookwhere "); Codec oe= new OracleCodec(); if(queryCondition!= null&& !queryCondition.isEmpty()){ for(Expression e : queryCondition){ String exprString=e.getColumn() + e.getOperator() + e.getValue(); String safeExpr= ESAPI.encoder().encodeForSQL(oe, exprString); sb.append(safeExpr).append(" and "); } sb.append("1=1"); Statement stat = connection.createStatement(); ResultSet rs= stat.executeQuery(sb.toString()); //other omitted code } } } 复制代码规则1.3 禁止直接使用不可信数据来拼接XML一个用户,如果他被允许输入结构化的XML片段,则他可以在XML的数据域中注入XML标签来改写目标XML文档的结构与内容。XML解析器会对注入的标签进行识别和解释。private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{ String xmlString; xmlString= "<user><role>operator</role><id>" + user.getUserId()+ "</id><description>" + user.getDescription() + "</description></user>"; outStream.write(xmlString.getBytes()); outStream.flush();}} 复制代码添加joeadministratorjoe<user> <role>operator</role> <id>joe</id> <role>administrator</role> <id>joe</id> <description>I want to be an administrator</description> </user> 复制代码XML Schema或者DTD校验,反例:private void createXMLStream(BufferedOutputStreamoutStream, User user)throwsIOException{ String xmlString; xmlString= "<user><id>" + user.getUserId()+ "</id><role>operator</role><description>"+ user.getDescription() + "</description></user>"; StreamSource xmlStream= new StreamSource(new StringReader(xmlString)); // Build a validating SAX parser using the schema SchemaFactory sf = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); StreamSource ss= new StreamSource(newFile("schema.xsd")); try{ Schema schema= sf.newSchema(ss); Validator validator= schema.newValidator(); validator.validate(xmlStream); }catch(SAXException x){ throw new IOException("Invalid userId", x); } // the XML is valid, proceed outStream.write(xmlString.getBytes()); outStream.flush(); } <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="user"> <xs:complexType> <xs:sequence> <xs:elementname="id" type="xs:string"/> <xs:element name="role"type="xs:string"/> <xs:element name="description" type="xs:string"/> </xs:sequence> </xs:complexType> </xs:element> </xs:schema> 复制代码某个恶意用户可能会使用下面的字符串作为用户ID:"joe</id>Administrator</role><!—"并使用如下字符串作为描述字段:"-><description>I want to be an administrator"<user> <id>joe</id> <role>Administrator</role><!--</id> <role>operator</role> <description> --> <description>I want to be an administrator</description> </user> 复制代码安全做法:白名单+安全的xml库private void createXMLStream(BufferedOutputStreamoutStream, User user) throws IOException{ // Write XML string if userID contains alphanumeric and underscore characters only if (!Pattern.matches("[_a-bA-B0-9]+", user.getUserId())){ // Handle format violation } if (!Pattern.matches("[_a-bA-B0-9]+", user.getDescription())){ // Handle format violation } String xmlString= "<user><id>"+ user.getUserId()+ "</id><role>operator</role><description>"+ user.getDescription() + "</description></user>"; outStream.write(xmlString.getBytes()); outStream.flush(); } public static void buidlXML(FileWriterwriter, User user) throwsIOException{ Document userDoc= DocumentHelper.createDocumen(); Element userElem= userDoc.addElement("user"); Element idElem= userElem.addElement("id"); idElem.setText(user.getUserId()); Element roleElem= userElem.addElement("role"); roleElem.setText("operator"); Element descrElem=userElem.addElement("description"); descrElem.setText(user.getDescription()); XMLWriter output = null; try{ OutputFormat format = OutputFormat.createPrettyPrint(); format.setEncoding("UTF-8"); output = new XMLWriter(writer, format); output.write(userDoc); output.flush(); } } 复制代码Xml注入净化之后的数据<user> <id>joe&lt;/id&gt;&lt;role&gt;Administrator&lt;/role&gt;&lt;!—</id> <role>operator</role> <description>--&gtlt;description&gt;Iwant to be an administrator</description> </user> 复制代码规则1.4:禁止直接使用不可信数据来记录日志如果在记录的日志中包含未经校验的不可信数据,则可能导致日志注入漏洞。恶意用户会插入伪造的日志数据,从而让系统 管理员误以为这些日志数据是由系统记录的。例如,一个用户可能通过输入一个回车符和一个换行符(CRLF)序列来将一 条合法日志拆分成两条日志,其中每一条都可能会令人误解。将未经净化的用户输入写入日志还可能会导致向信任边界之外泄露敏感数据,或者导致违反当地法律法规,在日志中写入和存储了某些类型的敏感数据。if(loginSuccessful){ logger.severe("User login succeeded for: "+ username); }else{ logger.severe("User login failed for: "+ username); } 复制代码生成log:david May 15, 2011 2:25:52 PM java.util.logging.LogManager$RootLogger.log SEVERE: User login succeeded for: administrator May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log SEVERE: User login failed for: david May 15, 2011 2:25:52 PM java.util.logging.LogManager log SEVERE: User login succeeded for: administrator Username=David(生成标准日志) May 15, 2011 2:19:10 PM java.util.logging.LogManager$RootLogger log SEVERE: User login failed for: david 复制代码登录之前会对用户名输入进行净化,从而防止注入攻击if(!Pattern.("[A-Za-z0-9_]+", username)){ // Unsanitized username logger.severe("User login failed for unauthorized user"); }else if(loginSuccessful){ logger.severe("User login succeeded for: "+ username); }else{ logger.severe("User login failed for: "+ username); } 复制代码规则1.5:禁止向Runtime.exec() 方法传递不可信、未净化的数据在执行任意系统命令或者外部程序时使用了未经校验的不可信输入,就会导致产生命令和参数注入漏洞。class DirList{ public static void main(String[] args){ if(args.length== 0){ System.out.println("No arguments"); System.exit(1); } try{ Runtime rt= Runtime.getRuntime(); Process proc = rt.exec("cmd.exe /c dir" + args[0]); // ... }catch(Exception e){ // Handle errors } } } 复制代码java DirList"dummy & echo bad"dirdummy echo bad安全建议:避免直接使用Runtime.exec(),采用标准的API替代运行系统命令来完成任务白名单数据校验和数据净化class DirList{ public static void main(String[] args){ if(args.length== 0){ System.out.println("No arguments"); System.exit(1); } try{ File dir= newFile(args[0]); // the dir need to be validated if (!validate(dir)) { System.out.println("An illegal directory"); }else{ for (String file : dir.list()){ System.out.println(file); } } } } } 复制代码类型举例常见注入模式和结果管道|| shell_command -执行命令并返回命令输出信息内联; &; shell_command -执行命令并返回命令输出信息& shell_command -执行命令并返回命令输出信息逻辑运算符$ &&||$(shell_command) -执行命令 && shell_command -执行命令并返回命令输出信息|| shell_command -执行命令并返回命令输出信息重定向运算符> >> <> target_file -使用前面命令的输出信息写入目标文件 >> target_file -将前面命令的输出信息附加到目标文件 < target_file-将目标文件的内容发送到前面的命令规则1.6:验证路径之前应该先将其标准化绝对路径名或者相对路径名中可能会包含文件链接,对文件名标准化可以使得验证文件路径更加容易,同时可以防御目录遍历引发的安全漏洞。public static void main(String[] args){ File f = newFile(System.getProperty("user.home") + System.getProperty("file.separator") + args[0]); String absPath= f.getAbsolutePath(); if(!isInSecureDir(Paths.get(absPath))){ // Refer to Rule 3.5 for the details of isInSecureDir() throw new IllegalArgumentException(); } if(!validate(absPath)){ // Validation throw new IllegalArgumentException(); } /* … */ } public static void main(String[] args) throwsIOException{ File f = newFile(System.getProperty("user.home") + System.getProperty("file.separator") + args[0]); String canonicalPath= f.getCanonicalPath(); if(!isInSecureDir(Paths.get(absPath))){ // Refer to Rule3.5 for the details of isInSecureDir() throw new IllegalArgumentException(); } if(!validate(absPath)){ // Validation throw new IllegalArgumentException(); } /* ... */ } 复制代码规则1.7:安全地从ZipInputStream提取文件提取出的文件标准路径落在解压的目标目录之外-跨目录解压攻击,是提取出的文件消耗过多的系统资源-zip压缩炸弹。static final int BUFFER= 512; // ... public final void unzip(String fileName) throws java.io.IOException{ FileInputStream fis= new FileInputStream(fileName); ZipInputStream zis= new ZipInputStream(newBufferedInputStream(fis)); ZipEntry entry; while((entry = zis.getNextEntry()) != null){ System.out.println("Extracting: "+ entry); int count; byte data[] = newbyte[BUFFER]; // Write the files to the disk FileOutputStreamfos= new FileOutputStream(entry.getName()); BufferedOutputStreamdest= new BufferedOutputStream(fos, BUFFER); while((count = zis.read(data, 0, BUFFER)) != -1){ dest.write(data, 0, count); } dest.flush(); dest.close(); zis.closeEntry(); } zis.close(); } 复制代码未对解压的文件名做验证,直接将文件名传递给FileOutputStream构造器。它也未检查解压文件的资源消耗情况,它允许程序运行到操作完成或者本地资源被耗尽示例public static final int BUFFER= 512; public static final int TOOBIG= 0x6400000; // 100MB public final void unzip(String filename) throws java.io.IOException{ FileInputStream fis= newFileInputStream(filename); ZipInputStreamzis= newZipInputStream(newBufferedInputStream(fis)); ZipEntry entry; try{ while((entry = zis.getNextEntry()) != null){ System.out.println("Extracting: "+ entry); int count; byte data[] = new byte[BUFFER]; if (entry.getSize() > TOOBIG){ throw new IllegalStateException("File to be unzipped is huge."); } if(entry.getSize() == -1){ throw new IllegalStateException("File to be unzipped might be huge."); } FileOutputStreamfos= newFileOutputStream(entry.getName()); BufferedOutputStreamdest= new BufferedOutputStream(fos,BUFFER); while((count = zis.read(data, 0, BUFFER)) != -1){ dest.write(data, 0, count); } dest.flush(); dest.close(); zis.closeEntry(); } } } 复制代码ZipEntry.getSize()方法在解压提取一个条目之前判断其大小,以试图解决之前的问题。攻击者可以伪造ZIP文件中用来描述解压条目大小的字段,因此,getSize()可靠的,本地资源实际仍可能被过度消耗static final int BUFFER= 512; static final int TOOBIG= 0x6400000; // max size of unzipped data, 100MB static final int TOOMANY = 1024; // max number of files // ... private String sanitzeFileName(String entryName, String intendedDir) throws IOException{ File f = newFile(intendedDir, entryName); String canonicalPath= f.getCanonicalPath(); File iD= newFile(intendedDir); String canonicalID= iD.getCanonicalPath(); if(canonicalPath.startsWith(canonicalID)){ return canonicalPath; }else{ throw new IllegalStateException("File is outside extraction target directory."); } } public final void unzip(String fileName) throws java.io.IOException{ FileInputStream fis= new FileInputStream(fileName); ZipInputStream zis= newZipInputStream(newBufferedInputStream(fis)); ZipEntryentry; int entries = 0; int total = 0; byte[] data = newbyte[BUFFER]; try{ while((entry = zis.getNextEntry()) != null){ System.out.println("Extracting: "+ entry); int count; String name = sanitzeFileName(entry.getName(), "."); FileOutputStream fos= newFileOutputStream(name); BufferedOutputStream dest= new BufferedOutputStream(fos, BUFFER); while (total + BUFFER<= && (count = zis.read(data, 0, BUFFER)) != -1){ dest.write(data, 0, count); total += count; } dest.flush(); dest.close(); zis.closeEntry(); entries++; if(entries > TOOMANY){ throw new IllegalStateException("Too many files to unzip."); } if(total > TOOBIG){ throw new IllegalStateException("File being unzipped is too big."); } } } } 复制代码规则1.8:禁止未经验证的用户输入直接输出到html界面用户输入未经过验证直接输出到html界面容易导致xss注入攻击,该攻击方式可以盗取用户cookie信息,严重的可以形成xss蠕虫攻击漏洞,也可以结合其他的安全漏洞进一步进行攻击和破坏系统反例:String eid=request.getParameter("eid"); eid=StringEscapeUtils.escapeHtml(eid);//insufficient validation ... ServletOutputStream out=response.getOutputStream(); out.print("Employee ID:"+eid); ... out.close(); ... 复制代码正例:... Statement stmt=conn.creatStatement(); ResultSet rs=stmt.executeQuery("select * from emp where id ="+eid); if(rs != null){ rs.next(); String name=StringEscapeUtils.escapeHtml(rs.getString("name"));//insufficient validation } ServletOutputStream out =response.getOutputStream(); ... out.close(); ... 复制代码数据类型上下文示例代码防御措施stringHTML Body<span>UNTRUSTED DATA</span>HTML Entity编码String安全HTML变量<input type="text" name="fname" value="UNTRUSTED DATA">1. HTML Attribute编码2. 只把不可信数据放在安全白名单内的变量上(白名单在下文列出)3. 严格地校验不安全变量,如background、id和nameStringGET参数<a href="/site/search?value=UNTRUSTED DATA">clickmeURL编码String使用在src或href变量上的不可信URLs<a href="UNTRUSTED URL">clickme</a><iframe src="UNTRUSTED URL" /1. 对输入进行规范化;2. URL校验;3. URL安全性认证4. 只允许使用http和https协议(避免使用JavaScript协议去打开一个新窗口)5. HTML Attribute编码StringCSS值<div style="width: UNTRUSTED DATA;">Selection</div>1. 使用CSS编码;2. 使用CSS Hex编码;3. 良好的CSS设计StringJavaScript变量<script> var currentValue='UNTRUSTED DATA';</script><script>someFunction('UNTRUSTED DATA');</script>1. 确保所有变量值都被引号括起来;2. 使用JavaScript Hex编码3. 使用JavaScript Unicode编码;4. 避免使用“反斜杠转译”(\"、\'或者\)HTMLHTML Body<div>UNTRUSTED HTML</div>[HTML校验(JSoup, AntiSamy, HTML Sanitizer)]StringDOM XSS<script> document.write("UNTRUSTED INPUT: " + document.location.hash);<script/>基于DOM操作的XSS漏洞防御措施1、输入过滤:客户端求情参数:包括用户输入,url参数、post参数。在产品形态上,针对不同输入类型,对输入做变量类型限制。字符串类型的数据,需要针对<、>、/、’、”、&五个字符进行实体化转义2、输出编码:浏览器解析中html和js编码不一样,以及上下文场景多样,所以对于后台输出的变量,不同的上下文中渲染后端变量,转码不一样。特殊字符实体编码&&amp<&lt>&gt“&quot/&#x2F‘&#x27规则1.9:禁止直接解析未验证的xml实体当允许引用外部实体时,若程序针对输入xml实体未验证,攻击者通过构造恶意内容,进行xxe注入攻击,可导致读取任意文件、执行系统命令、探测内网端口、攻击内网网站等危害 反例:public void transform(InputStream xmlStream,OutputStream output)throws Exception{ Transformer trans=null; TransformerFactory transFactory=TransformerFactory.newInstance(); if(this.style!=null){ trans=transFactory.newTranformer(this.style); }else{ trans=transFactory.newTranformer(); } /*********UTF-8***/ trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8"); Source source=new SAXSource(new InputSource(xmlStream)); Result result=new StreamResult(output); trans.transform(source,result); } 复制代码正例:public void transform(InputStream xmlStream,OutputStream output)throws Exception{ Transformer trans=null; TransformerFactory transFactory=TransformerFactory.newInstance(); if(this.style!=null){ //trans=transFactory.newTranformer(this.style); TransformerFactory trans=TransformerFactory.newInstance(); trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true); trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,""); trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,""); }else{ //trans=transFactory.newTranformer(); TransformerFactory trans=TransformerFactory.newInstance(); trfactory.setFeature(XMLConstans.FEATURE_SECURE_PROCESSING,true); trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_DTD,""); trfactory.setAttribute((XMLConstans.ACCESS_EXTERNAL_STYLESHEET,""); } /*********UTF-8***/ trans.setOutputProperty(OutputKeys.ENCOOING,"UTF-8"); Source source=new SAXSource(new InputSource(xmlStream)); Result result=new StreamResult(output); trans.transform(source,result); } 复制代码不同xml解析器防御xxe注入的方法:XMLReader To protect a java org.xml.sax.XMLReader from XXE,do this:XMLReader reader=XMLReaderFactory.createXMLReader(); reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true); reader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true); //stictly required as DTDs should not be allowed at all ,per previous reader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false); reader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false); 复制代码SAXReader To protect a java org.dom4j.io.SAXReader from XXE,do this:saxReader.setFeatrue("http://apache.org/xml/features/disallow-doctype-decl",true); saxReader.setFeatrue("http://xml.org/sax/features/external-general-entilies",false); saxReader.setFeatrue("http://xml.org/sax/features/external-parameter-entilies",false); 复制代码后续会把涉及的其他安全问题全部写出来,可关注本人的下篇文章。
《实战演练,拒绝996-工具IntelliJ IDEA篇》 工作中IntelliJ IDEA使用大全IntelliJ在业界被公认为最好的java开发工具之一,尤其在智能代码助手、代码自动提示、重构、J2EE支持、各类版本工具(git、svn等)、JUnit、CVS整合、代码分析、 创新的GUI设计等方面的功能可以说是超常的。因此我们必须熟悉IDEA的各自使用操作方法,使得我们在使用IDEA更加流畅,简便,以此来提高工作效率,减少996的加班、加班。如下是根据一系列的文章进行一些的总结,后续还会继续更新。IDEA激活过程首先我们必须进行软件激活破解,这样可能方便我们使用IDEA,步骤如下:安装完成后,先不要打开软件,将破解文件“JetbrainsCrack.jar”复制到软件安装目录【C:\Program Files\JetBrains\IntelliJ IDEA 2018.1\bin再用记事本打开“idea64.exe.vmoptions”和“idea.exe.vmoptions”这两个文件,再后面添加破解文件的路径,即是-javaagent:C:\Program Files\JetBrains\IntelliJ IDEA 2018.1\bin\JetbrainsCrack.jar。现在进行桌面上生成的intellij idea 2018.1快捷方式,选择do not import settings点击ok;阅读软件协议,拖动滑块,再点击accept输入产品注册码,选择activation code,将注册码复制进去即可。其中intellij idea 2018.1注册码如下:ThisCrackLicenseId-{ "licenseId":"ThisCrackLicenseId", "licenseeName":"Admin", "assigneeName":"", "assigneeEmail":"avxhm.se@gmail.com", "licenseRestriction":"Admin", "checkConcurrentUse":false, "products":[ {"code":"II","paidUpTo":"2099-12-31"}, {"code":"DM","paidUpTo":"2099-12-31"}, {"code":"AC","paidUpTo":"2099-12-31"}, {"code":"RS0","paidUpTo":"2099-12-31"}, {"code":"WS","paidUpTo":"2099-12-31"}, {"code":"DPN","paidUpTo":"2099-12-31"}, {"code":"RC","paidUpTo":"2099-12-31"}, {"code":"PS","paidUpTo":"2099-12-31"}, {"code":"DC","paidUpTo":"2099-12-31"}, {"code":"RM","paidUpTo":"2099-12-31"}, {"code":"CL","paidUpTo":"2099-12-31"}, {"code":"PC","paidUpTo":"2099-12-31"} ], "hash":"2911276/0", "gracePeriodDays":7, "autoProlongated":false} 复制代码常用配置自动编译在IDEA中进行手动打开自动编译设置,不需要每次写完代码后又要进行手动编译。 界面设置:File-->Settings-->Build,Execution,Deployment-->Compiler, 勾选✔Compiler中Build project automatically方法间的分隔符让代码阅读起来,美观,简洁;让方法之间分隔,方便管理,条理清晰,思路也清晰。界面设置:File-->Settings-->Editor-->General-->Appearance,勾选✔Appearance中的Show method separators忽略大小写打开了此开关,在码代时可以不区别大小写智能补充以及代码提示。界面设置File-->Settings-->Code Completion ,在Case sensitive completion 选择None即可。智能导包可以将自动导入不明确的结构以及智能优化包打开,可以实现写入一个jar的类时会自动导入该类的包。界面设置File-->Settings-->Auto Import,勾选✔Auto Import中Add unambiguous imports on the fly以及optimize import on the fly(for current project)悬浮提示打开了此配置,只要把鼠标放在相应的类上,就会出现提示。界面设置File-->Settings-->Editor-->General, 勾选✔General中Show quick document on mouse move设置显示文件tabs显示文件的Tabs设置,可以将打开的文件排列到左边,并进行tab数量的改变,如下图所需,这样操作起来方便简洁。界面设置File-->Settings-->Editor--> Editor Tabs,在Placement设置Left以及Tab limit为20即可。项目文件编码在文件中输入文字时会自动的转换为Unicode编码,然后在IDEA中开发文件时会自动转为文字显示,这样可以防止文件乱码。界面设置File-->Settings-->Editor-->File Encodeings,勾选✔File Encodeings中的Transparent native-to ascii conversation滚轴修改字体大小配置滚轴修改字体大小,可以方便快捷的Ctrl+滚轴改变字体大小。界面设置 File-->Settings-->Editor-->General,勾选✔General中的Change font size(Zoom) with Ctrl+Mouse Wheel设置行号显示显示行数可以在debug出现错误快日志中快速定位到出错的行数,并进行排查提高效率。界面设置File-->Settings-->Editor-->General-->Appearance,勾选✔Appearance中的Show line numbers文件过滤在打开文件时,可以过滤到不需要的文件,防止文件太多,杂乱,过滤的类型,区分大小写,一般可以过滤这些 CVS;SCCS;RCS;rcs;.DS_Store;.svn;.pyc;.pyo;.pyc;.pyo;.git;.hprof;_svn;.sbas;.IJI.;vssver.scc;vssver2.scc;.;.iml;.ipr;.iws;*.ids界面设置File-->Settings-->File Types,在Ignoe Files and folders中进行设置修改为Eclipse快捷键在IDEA中也可以将快捷键设置成Eclipse中的快捷键,为的就是方便从Eclipse中转到IDEA的进行快速熟悉操作。界面设置File-->Settings-->Keymap中修改为Eclipse用*标识编辑过的文件在IDEA中,你需要做以下设置, 这样被修改的文件会以*号标识出来,你可以及时保存相关的文件。界面设置File-->Settings-->Editor-->General-->Editor Tabs,勾选✔Editor Tabs中的Mark modified tabs with asterisk自动换行自动换行配置看个人喜爱,可设可不设。界面设置:File-->Settings-->Editor-->General,勾选✔General中的Use solt wraps in editor插件安装打开Setting-->Plugins,分别为Jetbrains插件、第三方插件、本地已下载的插件包安装。常用插件BackGround Image PlusIdea背景修改插件,可以设置自己喜欢得图片作为背景图片。插件下载:plugins.jetbrains.com/plugin/8502…REST client可以不使用PostMan等工具,可以使用自带工具Rest Client可以进行Restful webservice测试。插件下载:plugins.jetbrains.com/plugin/5951…UML Support内置 UML Support 插件可以很好地查看类继承关系,方便整理思路。LombokLombok为实体类提供get、set方法的lombok注解(@Setter@Getter、@Data等),减少代码维护的成本。插件下载:plugins.jetbrains.com/plugin/6317…CodeMakerCodeMaker代码生成工具,支持增加自定义代码模板(Velocity),支持选择多个类作为代码模板的上下文。插件下载:github.com/x-hansong/C…JUnitGeneratorJUnitGenerator单元测试是必不可少的!我们可以使用 JUnitGenerator 插件来自动创建了单元测试。插件下载:plugins.jetbrains.com/plugin/3064…Mybatis插件Free Mybatis plugin,在开发中过程中在mapper接口中方法与对应xml的sql语句互动需要靠搜索查询,该插件提供了便捷两者之间跳转访问。插件下载:plugins.jetbrains.com/plugin/8321…Alibaba Java Code Guidelines阿里代码规约检测,根据阿里巴巴java开发手册规范进行检查代码,更好保证代码的规范化。插件下载plugins.jetbrains.com/plugin/1004…Maven HelperMaven 引入的 jar 包有冲突,可以使用 Maven Helper 插件来帮助分析。插件下载:plugins.jetbrains.com/plugin/7179…FindBugs-IDEAFindBugs-IDEA使用静态分析来 查找 Java 代码中的错误 的程序。插件下载:plugins.jetbrains.com/plugin/3847…翻译插件Translation在阅读源码时,遇到不认识的英文可以使用翻译插件Translation,使用方法快捷节ATL+1。插件下载:plugins.jetbrains.com/plugin/8579…GsonFormatjson格式的数据转成Java Object,使用方法复制好需要解析的Json数据,alt+insert/alt+s开启。插件下载:github.com/zzz40500/Gs…Key promoterIntelliJ IDEA的快捷键提示插件,会根据用户行为记录某功能,并提示下次使用什么快捷键。插件下载:plugins.jetbrains.com/plugin/9792…POJO to JSONPOJO to JSON可将简单 Java 领域对象转成 JSON 字符串方便用 postman 或者 curl 模拟数据。插件下载:plugins.jetbrains.com/plugin/9686…字符串工具:String ManipulationString Manipulation提供了非常丰富字符串工具,例如命名替换( (camelCase, kebab-lowercase, KEBAB-UPPERCASE, snakecase, SCREAMINGSNAKE_CASE, dot.case, words lowercase, Words Capitalized, PascalCase)等。插件下载:plugins.jetbrains.com/plugin/2162…RESTful 服务开发辅助工具集: RestfulToolkit开发中,经过会根据 URI 的部分信息来查找对应的 Controller 中方法,RestfulToolkit 提供了一套 RESTful 服务开发辅助工具集,提供了如下功能:根据 URL 直接跳转到对应的方法定义 ( Ctrl \ or Ctrl Alt N ); 提供了一个 Services tree 的显示窗口;一个简单的 http 请求工具;在请求方法上添加了有用功能: 复制生成 URL;复制方法参数...其他功能: java 类上添加 Convert to JSON 功能,格式化 json 数据 ( Windows: Ctrl + Enter; Mac: Command + Enter )。插件下载:plugins.jetbrains.com/plugin/1029…Redis可视化:IedisIedis可方便的执行增删查改及使用命令行进行操作。插件下载:plugins.jetbrains.com/plugin/9228…快捷键查询快捷键快捷键说明CTRL+N查找类CTRL+SHIFT+N查找文件CTRL+SHIFT+ALT+N查找类中的方法或变量ALT+F7查看变量调用情况CIRL+B查询变量的来源CTRL+ALT+B找所有的子类CTRL+SHIFT+B找变量的类CTRL+G定位行CTRL+F在当前窗口查找文本CTRL+SHIFT+F在指定窗口查找文本CTRL+R在当前窗口替换文本CTRL+SHIFT+R在指定窗口替换文本ALT+SHIFT+C查找修改的文件CTRL+E最近打开的文件Alt+Shift+C对比最近修改的代码F3向下查找关键字出现位置SHIFT+F3向上一个关键字出现位置F4查找变量来源CTRL+ALT+F7选中的字符查找工程出现的地方CTRL+SHIFT+O弹出显示查找内容自动代码快捷键说明ALT+回车导入包,自动修正CTRL+ALT+L格式化代码CTRL+ALT+I自动缩进CTRL+ALT+O优化导入的类和包ALT+INSERT生成代码(如GET,SET方法,构造函数等)CTRL+SHIFT+SPACE自动补全代码CTRL+空格代码提示CTRL+ALT+SPACE类名或接口名提示CTRL+P方法参数提示CTRL+J自动代码CTRL+ALT+T把选中的代码放在 TRY{} IF{} ELSE{}里CTRL+ALT+M抽取方法其他快捷方式快捷键说明Ctrl+B快速打开光标处的类或方法Ctrl+O查看该类可以重写哪些方法CTRL+D复制行CTRL+X剪切,删除行CIRL+U大小写切换Ctrl+Shift+U大小写切换CTRL+Z撤回CTRL+SHIFT+Z回撤CTRL+/使用//注释CTRL+SHIFT+/使用/.../注释CTRL+W选中代码,一般选择一个单词CTRL+B快速打开光标处的类或方法CTRL+ALT+F12资源管理器打开文件夹ALT+F1查找文件所在目录位置SHIFT+ALT+INSERT竖编辑模式ALT+ ←/→切换代码视图CTRL+ALT ←/→返回上次编辑的位置ALT+ ↑/↓在方法间快速移动定位SHIFT+F6重构-重命名CTRL+H显示类结构图CTRL+ATL+H显示方法的调用关系CTRL+Q显示注释文档ALT+1快速打开或隐藏工程面板CTRL+SHIFT+UP/DOWN代码向上/下移动CTRL+UP/DOWN光标跳转到第一行或最后一行下ESC光标返回编辑框SHIFT+ESC光标返回编辑框,关闭无用的窗口Ctrl+ F9重新编译, 删除缓存.实时更新常用版本管理SVN与git以及项目管理Maven与Ant、tomacat配置MavenIntellij IDEA>File>Setting 打开设置,搜索maven 点击maven在右侧选择,在maven面板中进行如下配置:Ant下载好Ant,解压后进行环境变量的配置即可。如(在环境变量中配置变量ANT_HOME,值为H:\apache-ant-1.7.1;在Path中添加:%ANT_HOME%\bin;),最后验证Ant是否安装成功,开始–>运行–>cmd,进入命令行–>键入 ant -version回车,如图,便表示Ant配置完成。Git在File-->Setting->Version Control-->Git-->Path to Git executable选择你的git安装后的git.exe文件,然后点击Test,测试是否设置成功。在IDEA中设置GitHub,File-->Setting->Version Control-->GibHub,Host:github.com,Token:点击Create API Token,输入在github中注册的用户名和密码生成token点击Test,测试是否连接成功。代码下载:项目文件点击右键,选择git进行操作:SVN在File->Settings->Version Control->Subversion中设置,在Subversion右侧选择svn客户端安装路径bin目录下的svn.exe点击OK就配置完成了,操作以及下载代码跟Git差不多。配置如下:tomcat的JVM参数配置配置tomcat-Xms256m -Xmx2048m -XX:PermSize=128M -XX:MaxPermSize=1024M,经常需要配置堆、栈的内存大小,配置如下:使用中常出现的问题IntelliJ 强制更新Maven DependenciesIntelliJ自动载入Maven依赖的功能很好用,但可能会导致POM文件修改后却没有触发自动载入的动作,此时需要手动强制更新依赖。手动删除Project Setting里面的libraries内容在Maven Project的视图中进行clean 操作删除之前编译过的文件项目右键-->maven-->Reimport即可建立依赖。idea中maven编译出错问题idea中maven编译出错问题可查看:www.2cto.com/kf/201708/6…IDEA解决maven包冲突的一些小技巧IDEA解决maven包冲突的一些小技巧可查看:segmentfault.com/a/119000001…解决IntelliJ IDEA maven库下载依赖包速度慢的问题修改maven的镜像即可,可以通过右键项目选中maven选项,然后选择“open settings.xml”或者 “create settings.xml”,示例如下:<mirror> <id>alimaven</id> <name>aliyun maven</name> <url>http://maven.aliyun.com/nexus/content/groups/public/</url> <mirrorOf>central</mirrorOf> </mirror> 复制代码maven依赖问题使用Intellij IDEA分析解决maven依赖冲突问题可查看:blog.csdn.net/u013870094/…总结后续还有更多使用IDEA出现问题更新,也欢迎留言补充。
通过在不同的计算机上托管mongod实例来尽可能多地保持成员之间的分离。将虚拟机用于生产部署时,应将每个mongod实例放置在由冗余电源电路和冗余网络路径提供服务的单独主机服务器上,而且尽可能的将副本集的每个成员部署到自己的计算机绑定到标准的MongoDB端口27017。其中三个成员节点的副本集提供足够的冗余以承受大多数网络分区和其他系统故障。这些collection集合还具有足够的容量用于许多分布式读取操作。副本集应始终具有奇数个成员。这确保选举顺利进行。部署时考虑的问题:建立虚拟专用网络。确保您的网络拓扑通过局域网路由单个站点内的成员节点之间的所有流量。配置访问控制以防止从未知客户端到副本集的连接。配置网络和防火墙规则,以便仅在默认的MongoDB端口上允许传入和传出的数据包,并且仅在部署中允许。确保可通过可解析的DNS或主机名访问副本集的每个成员节点。您应该正确配置DNS名称,或者设置系统的/etc/hosts文件以反映此配置。每个成员必须能够连接到每个其他成员。NOTE:如果可能,请使用逻辑DNS主机名而不是IP地址,尤其是在配置副本集成员或分片集群成员时。逻辑DNS主机名的使用避免了由于IP地址更改而导致的配置更改。生产环境中禁用访问控制时部署副本集的步骤IP绑定:使用bind_ip选项可确保MongoDB侦听来自配置地址上的应用程序的连接。在绑定到非本地主机(例如可公开访问的)IP地址之前,请确保已保护您的群集免受未经授权的访问。例如,以下mongod实例绑定到localhost和主机名TestHostname,它与ip地址198.51.100.1相关联:mongod --bind_ip localhost,TestHostname 复制代码要连接到此实例,远程客户端必须指定主机名或其关联的IP地址198.51.100.1:mongo --host TestHostname mongo --host 198.51.100.1 复制代码创建MongoDB存储数据文件的目录,在/etc/mongod.conf中存储的配置文件或相关位置中指定mongod配置。使用适当的选项启动副本集的每个成员。对于每个成员,使用以下设置启动mongod实例:将replication.replSetName选项设置为副本集名称(如果您的应用程序连接到多个副本集,则每个集合应具有不同的名称,某些驱动程序按副本集名称对副本集进行连接)。将net.bindIp选项设置为hostname / ip或逗号分隔的hostnames / ips列表,以及配置部署所需的其他配置。以下示例通过--replSet和--bind_ip命令行选项指定副本集名称和ip绑定:mongod --replSet "rs0" --bind_ip localhost,<hostname(s)|ip address(es)> 复制代码对于<hostname(s)| ip address(es)>,指定远程客户端(包括副本集的其他成员)可用于连接的mongod实例的主机名和/或IP地址。 或者,也可以在配置文件中指定副本集名称和IP地址,要使用配置文件启动mongod,请使用--config选项指定配置文件的路径:replication: replSetName: "rs0" net: bindIp: localhost,<hostname(s)|ip address(es)> mongod --config <path-to-config> 复制代码使用mongo shell连接到其中一个mongod实例,从运行其中一个mongod的同一台机器(在本教程中为mongodb0.example.net)启动mongo shell。 要在默认端口27017上连接到监听localhost的mongod,只需指令mongo。启动副本集,仅在副本集的一个且仅一个mongod实例上运行rs.initiate(),如在副本集成员0上运行rs.initiate():rs.initiate( { _id : "rs0", members: [ { _id: 0, host: "mongodb0.example.net:27017" }, { _id: 1, host: "mongodb1.example.net:27017" }, { _id: 2, host: "mongodb2.example.net:27017" } ] }) 复制代码查看副本集配置,使用rs.conf()显示副本集配置对象。如副本集配置对象类似于以下内容:{ "_id" : "rs0", "version" : 1, "protocolVersion" : NumberLong(1), "members" : [ { "_id" : 0, "host" : "mongodb0.example.net:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 1, "host" : "mongodb1.example.net:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 2, "host" : "mongodb2.example.net:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 } ], "settings" : { "chainingAllowed" : true, "heartbeatIntervalMillis" : 2000, "heartbeatTimeoutSecs" : 10, "electionTimeoutMillis" : 10000, "catchUpTimeoutMillis" : -1, "getLastErrorModes" : { }, "getLastErrorDefaults" : { "w" : 1, "wtimeout" : 0 }, "replicaSetId" : ObjectId("585ab9df685f726db2c6a840") } } 复制代码确保副本集具有主副本,使用rs.status()查看复制集的主节点。测试以及开发环境中复制集的应用部署在此测试以及开发部署中,三个成员在同一台计算机上运行。IP绑定:使用bind_ip选项可确保MongoDB侦听来自配置地址上的应用程序的连接。在绑定到非本地主机(例如可公开访问的)IP地址之前,请确保已保护您的群集免受未经授权的访问。例如,以下mongod实例绑定到localhost和主机名TestHostname,它与ip地址198.51.100.1相关联:mongod --bind_ip localhost,TestHostname 复制代码要连接到此实例,远程客户端必须指定主机名或其关联的IP地址198.51.100.1:mongo --host TestHostname mongo --host 198.51.100.1 复制代码通过发出类似于以下内容的命令为每个成员创建必要的数据目录,这将创建名为“rs0-0”,“rs0-1”和“rs0-2”的目录,它们将包含实例的数据库文件,示例如下:mkdir -p / srv / mongodb / rs0-0 / srv / mongodb / rs0-1 / srv / mongodb / rs0-2 复制代码通过发出以下命令在自己的shell窗口中启动mongod实例://不同的三个节点: mongod --replSet rs0 --port 27017 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /srv/mongodb/rs0-0 --smallfiles --oplogSize 128 mongod --replSet rs0 --port 27018 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /srv/mongodb/rs0-1 --smallfiles --oplogSize 1284 mongod --replSet rs0 --port 27019 --bind_ip localhost,<hostname(s)|ip address(es)> --dbpath /srv/mongodb/rs0-2 --smallfiles --oplogSize 128 复制代码这将每个实例作为名为rs0的副本集的成员启动,每个副本集都在不同的端口上运行,并使用--dbpath设置指定数据目录的路径。 如果已在使用上述端口,请选择其他的端口。实例绑定到localhost和主机的ip地址。--smallfiles和--oplogSize设置可减少每个mongod实例使用的磁盘空间。 [1]这是测试和开发部署的理想选择,因为它可以防止机器过载。 有关这些和其他配置选项的更多信息,请参阅<配置文件选项>(docs.mongodb.com/manual/refe…)通过mongo shell连接到其中一个mongod实例,需要通过指定端口号来指示哪个实例。 假设选择第一个,如下面的命令:mongo --port 27017 复制代码在mongo shell中,使用rs.initiate()来启动副本集,主要是在mongo shell环境中创建副本集配置对象,并用系统的主机名替换,然后将rsconf文件传递给rs.initiate(),如以下示例所示:rsconf = { _id: "rs0", members: [ { _id: 0, host: "<hostname>:27017" }, { _id: 1, host: "<hostname>:27018" }, { _id: 2, host: "<hostname>:27019" } ] } rs.initiate( rsconf ) 复制代码通过发出rs.conf()命令显示当前副本配置:rs.conf(),使用rs.status()操作随时检查副本集的状态。副本集配置对象类似于以下内容:{ "_id" : "rs0", "version" : 1, "protocolVersion" : NumberLong(1), "members" : [ { "_id" : 0, "host" : "<hostname>:27017", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 1, "host" : "<hostname>:27018", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 }, { "_id" : 2, "host" : "<hostname>:27019", "arbiterOnly" : false, "buildIndexes" : true, "hidden" : false, "priority" : 1, "tags" : { }, "slaveDelay" : NumberLong(0), "votes" : 1 } ], "settings" : { "chainingAllowed" : true, "heartbeatIntervalMillis" : 2000, "heartbeatTimeoutSecs" : 10, "electionTimeoutMillis" : 10000, "catchUpTimeoutMillis" : -1, "getLastErrorModes" : { }, "getLastErrorDefaults" : { "w" : 1, "wtimeout" : 0 }, "replicaSetId" : ObjectId("598f630adc9053c6ee6d5f38") } } 复制代码仲裁节点添加到副本集仲裁节点是mongod实例,它们是副本集的一部分但不保存数据。仲裁节点参加选举以打破关系。如果副本集具有偶数个成员,请添加仲裁者。仲裁节点具有最低的资源要求,不需要专用硬件。您可以在应用程序服务器或监视主机上部署仲裁程序。不要在也承载副本集的主成员节点或辅助成员节点的系统上运行仲裁程序。仲裁节点不存储数据,但是在将仲裁节点的mongod进程添加到副本集之前,仲裁节点将像任何其他mongod进程一样运行并启动一组数据文件和一个完整大小的日志。为仲裁节点创建数据目录(例如storage.dbPath)。 mongod实例使用该目录进行配置数据。 该目录不会保存数据集。 例如,创建/ data / arb目录:mkdir /data/arb 复制代码启动仲裁节点,指定数据目录和要加入的副本集的名称。 以下开始使用/ data / arb作为dbPath和rs作为副本集名称的仲裁:mongod --port 27017 --dbpath /data/arb --replSet rs --bind_ip localhost,<hostname(s)|ip address(es)>连接到主服务器并将仲裁服务器添加到副本集。,使用rs.addArb()方法,如以下示例所示,该示例假定m1.example.net是与仲裁节点的指定ip地址关联的主机名:rs.addArb("m1.example.net:27017") //此操作添加在m1.example.net主机上的端口27017上运行的仲裁器。 复制代码单实例转变为复制集将独立mongod实例转换为副本集的过程。使用独立实例进行测试和开发,但始终在生产中使用副本集。该过程特定于不属于分片群集的实例。具体过程如下:关闭单实例的mongod。重启实例。 使用--replSet选项指定新副本集的名称。例如,以下命令将独立实例作为名为rs0的新副本集的成员启动。 该命令使用独立的/ srv / mongodb / db0的现有数据库路径:mongod --port 27017 --dbpath /srv/mongodb/db0 --replSet rs0 --bind_ip localhost,<hostname(s)|ip address(es)> 复制代码将mongo shell连接到mongod实例。使用rs.initiate()启动新的副本集:rs.initiate() 副本集现在可以运行了。 要查看副本集配置,请使用rs.conf()。 要检查副本集的状态,请使用rs.status()。增加新节点副本集最多可以有七个投票成员。要将成员添加到已有七个投票成员的副本集,您必须将该成员添加为无投票成员或从现有成员中删除投票。您可以使用这些过程将新成员添加到现有集。您还可以使用相同的过程“重新添加”已删除的成员。如果删除的成员的数据仍然相对较新,很简单地恢复。如果您有现有成员的备份或快照,则可以将数据文件(例如dbPath目录)移动到新系统,并使用它们快速启动新成员。文件必须是来自同一副本集的成员的数据文件的有效副本。准备数据目录在将新成员添加到现有副本集之前,请使用以下策略之一准备新成员的数据目录:确保新成员的数据目录不包含数据。新成员将复制现有成员的数据。如果新成员处于恢复状态,则必须先退出并成为辅助成员,然后MongoDB才能在复制过程中复制所有数据。此过程需要时间,但不需要管理员干预。从现有成员手动复制数据目录。新成员将成为辅助成员,并将同步副本集的当前状态。复制数据可能会缩短新成员成为最新成员的时间。确保您可以将数据目录复制到新成员,并在oplog允许的窗口内开始复制。否则,新实例必须执行初始同步,这将完全重新同步数据,如重新同步副本集的成员中所述。使用rs.printReplicationInfo()检查副本集成员关于oplog的当前状态。具体步骤启动新的mongod实例。 指定数据目录和副本集名称。 以下示例指定/ srv / mongodb / db0数据目录和rs0副本集:mongod --dbpath /srv/mongodb/db0 --replSet rs0 --bind_ip localhost,<hostname(s)|ip address(es)> 复制代码可以在mongod.conf配置文件中指定数据目录,副本集名称和ip绑定,并使用以下命令启动mongod:mongod --config /etc/mongod.conf 复制代码连接到副本集的主节点,只能在连接到主节点时添加成员。 如果不知道哪个成员是主成员节点,请登录到副本集的任何成员并发出db.isMaster()命令。使用rs.add()将新成员添加到副本集。 将成员配置文档传递给方法。 例如,要在主机mongodb3.example.net上添加成员,请发出以下命令:rs.add( { host: "mongodb3.example.net:27017", priority: 0, votes: 0 } ) 复制代码确保新成员已达到SECONDARY状态。 要检查副本集成员的状态,请运行rs.status():rs.status()一旦新添加的成员转换为SECONDARY状态,请使用rs.reconfig()更新新添加的成员的优先级并根据需要进行投票。例如,如果rs.conf()返回mongodb3.example.net:27017的配置文档作为members数组中的第五个元素,要更新其优先级并投票为1,请使用以下操作序列:var cfg = rs.conf(); cfg.members[4].priority = 1 cfg.members[4].votes = 1 rs.reconfig(cfg) 复制代码NOTE:当新添加的辅助节点的投票和优先级设置大于零时,在其初始同步期间,辅助节点仍然计为投票成员,即使它不能提供读取也不能成为主节点,因为其数据尚未一致。这可能导致大多数投票成员在线但不能选出主要成员的情况。 要避免这种情况,请考虑最初添加新的辅助优先级:0和投票:0。 然后,一旦成员转换到SECONDARY状态,使用rs.reconfig()更新其优先级和投票。移除节点使用rs.remove()移除过程关闭要删除的成员的mongod实例。 要关闭实例,请使用mongo shell和db.shutdownServer()方法进行连接。连接到副本集的当前主节点。 要确定当前主节点,请在连接到副本集的任何成员时使用db.isMaster()。使用以下任一形式的rs.remove()删除该成员:rs.remove( “mongod3.example.net:27017”) rs.remove( “mongod3.example.net”) NOTE:如果副本集需要选择新的主节点,MongoDB可能会短暂地断开shell。 在这种情况下,shell会自动重新连接。 即使命令成功,shell也可能显示DBClientCursor :: init call()失败错误。使用rs.reconfig()移除过程关闭要删除的成员的mongod实例。 要关闭实例,请使用mongo shell和db.shutdownServer()方法进行连接。连接到副本集的当前主节点。 要确定当前主节点,请在连接到副本集的任何成员时使用db.isMaster()。发出rs.conf()方法以查看当前配置文档并确定要删除的成员的成员数组中的位置,示例mongod_C.example.net位于以下配置文件的第2位:{ "_id" : "rs", "version" : 7, "members" : [ { "_id" : 0, "host" : "mongod_A.example.net:27017" }, { "_id" : 1, "host" : "mongod_B.example.net:27017" }, { "_id" : 2, "host" : "mongod_C.example.net:27017" } ] 复制代码将当前配置文档分配给变量cfg,修改cfg对象以删除该成员,并重启新配置,示例如下:cfg = rs.conf() cfg.members.splice(2,1) rs.reconfig(cfg) 复制代码要确认新配置是否生效,请发出rs.conf()。例如,输出将是:{ "_id" : "rs", "version" : 8, "members" : [ { "_id" : 0, "host" : "mongod_A.example.net:27017" }, { "_id" : 1, "host" : "mongod_B.example.net:27017" } ] } 复制代码总结后续还有关于实践中分片的搭建过程,以及分片和复制集原理分析。
在使用MongoDB时,在创建索引会涉及到在复制集(replication)以及分片(Shard)中创建,为了最大限度地减少构建索引的影响,在副本和分片中创建索引,使用滚动索引构建过程。如果不使用滚动索引构建过程:主服务器上的前台索引构建需要数据库锁定。它复制为副本集辅助节点上的前台索引构建,并且复制工作程序采用全局数据库锁定,该锁定将读取和写入排序到索引服务器上的所有数据库。主要的后台索引构建复制为后台索引构建在辅助节点上。复制工作程序不会进行全局数据库锁定,并且辅助读取不会受到影响。对于主服务器上的前台和后台索引构建,副本集辅助节点上的索引操作在主节点完成构建索引之后开始。在辅助节点上构建索引所需的时间必须在oplog的窗口内,以便辅助节点可以赶上主节点。 那么该如何创建呢?具体步骤呢?请看接下来的具体过程。1. 在副本集创建索引准备必须在索引构建期间停止对集合的所有写入,否则可能会在副本集成员中获得不一致的数据。具体过程在副本集中以滚动方式构建唯一索引包括以下过程:停止一个Secondary节点(从节点)并以单机模式重新启动,可以使用配置文件更新配置以单机模式重新启动:注释掉replication.replSetName选项。将net.port更改为其他端口。将原始端口设置注释掉。在setParameter部分中将参数disableLogicalSessionCacheRefresh设置为true。例如://修改配置 net: bindIp: localhost,<hostname(s)|ip address(es)> port: 27217 #port: 27017 #replication: #replSetName: myRepl setParameter: disableLogicalSessionCacheRefresh: true //重新启动 mongod --config <path/To/ConfigFile> 复制代码创建索引:在单机模式下进行索引创建重新开启Replica Set 模式:索引构建完成后,关闭mongod实例。撤消作为独立启动时所做的配置更改,以返回其原始配置并作为副本集的成员重新启动。//回退原来的配置:net: bindIp: localhost,<hostname(s)|ip address(es)> port: 27017 replication: replSetName: myRepl //重新启动: mongod --config <path/To/ConfigFile> 复制代码在其他从节点中重复1、2、3步骤的过程操作。主节点创建索引,当所有从节点都有新索引时,降低主节点,使用上述过程作为单机模式重新启动它,并在原主节点上构建索引:使用mongo shell中的rs.stepDown()方法来降低主节点为从节点,成功降级后,当前主节点成为从节点,副本集成员选择新主节点,并进行从节点创建方式进行创建索引。2. 分片集群创建唯一索引准备创建唯一索引,必须在索引构建期间停止对集合的所有写入。 否则,您可能会在副本集成员中获得不一致的数据。如果无法停止对集合的所有写入,请不要使用以下过程来创建唯一索引。具体过程停止Balancer:将mongo shell连接到分片群集中的mongos实例,然后运行sh.stopBalancer()以禁用Balancer。如果正在进行迁移,系统将在停止平衡器之前完成正在进行的迁移。确定Collection的分布:刷新该mongos的缓存路由表,以避免返回该Collection旧的分发信息。刷新后,对要构建索引的集合运行db.collection.getShardDistribution()。例如:在test数据库中的records字段中创建上升排序的索引db.adminCommand( { flushRouterConfig: "test.records" } ); db.records.getShardDistribution(); 复制代码例如,考虑一个带有3个分片shardA,shardB和shardC的分片集群,db.collection.getShardDistribution()返回以下内容Shard shardA at shardA/s1-mongo1.example.net:27018,s1-mongo2.example.net:27018,s1-mongo3.example.net:27018 data : 1KiB docs : 50 chunks : 1 estimated data per chunk : 1KiB estimated docs per chunk : 50 Shard shardC at shardC/s3-mongo1.example.net:27018,s3-mongo2.example.net:27018,s3-mongo3.example.net:27018 data : 1KiB docs : 50 chunks : 1 estimated data per chunk : 1KiB estimated docs per chunk : 50 Totals data : 3KiB docs : 100 chunks : 2 Shard shardA contains 50% data, 50% docs in cluster, avg obj size on shard : 40B Shard shardC contains 50% data, 50% docs in cluster, avg obj size on shard : 40B 从输出中,您只在shardA和shardC上为test.records构建索引。 复制代码在包含集合Chunks的分片创建索引C1.停止从节点,并以单机模式重新启动:对于受影响的分片,停止从节点与其中一个分区相关联的mongod进程,进行配置文件/命令模式更新后重新启动。配置文件:将net.port更改为其他端口。 注释到原始端口设置。注释掉replication.replSetName选项。注释掉sharding.clusterRole选项。在setParameter部分中将参数skipShardingConfigurationChecks设置为true。在setParameter部分中将参数disableLogicalSessionCacheRefresh设置为true。net: bindIp: localhost,<hostname(s)|ip address(es)> port: 27218 # port: 27018 #replication: # replSetName: shardA #sharding: # clusterRole: shardsvr setParameter: skipShardingConfigurationChecks: true disableLogicalSessionCacheRefresh: true //重启: mongod --config <path/To/ConfigFile> 复制代码C2.创建索引:直接连接到在新端口上作为独立运行的mongod实例,并为此实例创建新索引。//例如:在record Collection的username创建索引 db.records.createIndex( { username: 1 } ) 复制代码C3.恢复C1的配置,并作为 Replica Set成员启动:索引构建完成后,关闭mongod实例。 撤消作为单机模式时所做的配置更改,以返回其原始配置并重新启动。配置文件模式:恢复为原始端口号。取消注释replication.replSetName。取消注释sharding.clusterRole。删除setParameter部分中的参数skipShardingConfigurationChecks。在setParameter部分中删除参数disableLogicalSessionCacheRefresh。net: bindIp: localhost,<hostname(s)|ip address(es)> port: 27018 replication: replSetName: shardA sharding: clusterRole: shardsvr 重启:mongod --config <path/To/ConfigFile> 复制代码C4.其他从节点分片重复C1、C2、C3过程创建索引。C5.主节点创建索引:当所有从节点都有新索引时,降低主节点,使用上述过程作为单机模式重新启动它,并在原主节点上构建索引 使用mongo shell中的rs.stepDown()方法来降低主节点为从节点,成功降级后,当前主节点成为从节点,副本集成员选择新主节点,并进行从节点创建方式进行创建索引。在其他受影响的分片重复C步骤;重启Balancer,一旦全部分片创建完索引,重启Balancer:sh.startBalancer()。总结后续还有关于实践中复制集以及分片的搭建过程,复制集成员节点增加删除等一系列实战操作。
索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中( 索引存储在特定字段或字段集的值),而且是使用了B-tree结构。索引可以极大程度提升MongoDB查询效率。如果没有索引,MongoDB必须执行全集合collections扫描,即扫描集合中的每个文档,选取符合查询条件的文档document。 如果查询时存在适当的索引,MongoDB可以使用索引来限制它必须查询的文档document的数量,特别是在处理大量数据时,所以选择正确的索引是很关键的、重要的。创建索引,需要考虑的问题:每个索引至少需要数据空间为8kb;添加索引会对写入操作会产生一些性能影响。 对于具有高写入率的集合Collections,索引很昂贵,因为每个插入也必须更新任何索引;索引对于具有高读取率的集合Collections很有利,不会影响没索引查询;处于索引处于action状态时,每个索引都会占用磁盘空间和内存,因此需要对这种情况进行跟踪检测。索引限制:索引名称长度不能超过128字段;复合索引不能超过32个属性;每个集合Collection不能超过64个索引;不同类型索引还具有各自的限制条件。1. 索引管理1.1 索引创建索引创建使用createIndex()方法,格式如下:db.collection.createIndex(<key and index type specification>,<options>) 复制代码createIndex() 接收可选参数,可选参数列表如下:ParameterTypeDescriptionbackgroundBoolean建索引过程会阻塞其它数据库操作,background可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为false。uniqueBoolean建立的索引是否唯一。指定为true创建唯一索引。默认值为false.namestring索引的名称。如果未指定,MongoDB的通过连接索引的字段名和排序顺序生成一个索引名称。dropDupsBoolean3.0+版本已废弃。在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 false.sparseBoolean对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为true的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 false.expireAfterSecondsinteger指定一个以秒为单位的数值,完成 TTL设定,设定集合的生存时间。vindex version索引的版本号。默认的索引版本取决于mongod创建索引时运行的版本。weightsdocument索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。default_languagestring对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语language_overridestring对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的language,默认值为 language.1.2 查看索引查看Collection中所有索引,格式如下:db.collection.getIndexes() 复制代码1.3 删除索引删除Collection中的索引:格式如下:db.collection.dropIndexes() //删除所有索引 db.collection.dropIndex() //删除指定的索引 复制代码1.4 索引名称索引的默认名称是索引键和索引中每个键的value1或-1,形式index_name+1/-1,比如:db.products.createIndex( { item: 1, quantity: -1 } )----索引名称为item_1_quantity_-1 复制代码也可以指定索引名称:db.products.createIndex( { item: 1, quantity: -1 } , { name: "inventory" } ) ----索引名称为inventory 复制代码1.5 查看索引创建过程以及终止索引创建方法解析db.currentOp()查看索引创建过程db.killOp(opid)终止索引创建,其中-opid为操作id1.6 索引使用情况形式解析$indexStats获取索引访问信息explain()返回查询情况:在executionStats模式下使用db.collection.explain()或cursor.explain()方法返回有关查询过程的统计信息,包括使用的索引,扫描的文档数以及查询处理的时间(以毫秒为单位)。Hint()控制索引,例如要强制MongoDB使用特定索引进行db.collection.find()操作,请使用hint()方法指定索引1.7 MongoDB度量标准MongoDB提供了许多索引使用和操作的度量标准,在分析数据库的索引使用时可能需要考虑这些度量标准,如下所示:形式解析metrics.queryExecutor.scanned在查询和查询计划评估期间扫描的索引项的总数metrics.operation.scanAndOrder返回无法使用索引执行排序操作的已排序数字的查询总数collStats.totalIndexSize所有索引的总大小。 scale参数会影响此值。如果索引使用前缀压缩(这是WiredTiger的默认值),则返回的大小将反映计算总计时任何此类索引的压缩大小。collStats.indexSizes指定集合collection上每个现有索引的键和大小。 scale参数会影响此值dbStats.indexes包含数据库中所有集合的索引总数的计数。dbStats.indexSize在此数据库上创建的所有索引的总大小1.8 后台索引操作在密集(快达到数据库最大容量)Collection创建索引:在默认情况下,在密集的Collection(快达到数据库最大容量)时创建索引,会阻止其他操作。在给密集的Collection(快达到数据库最大容量)创建索引时, 索引构建完成之前,保存Collection的数据库不可用于读取或写入操作。 任何需要对所有数据库(例如listDatabases)进行读或写锁定的操作都将等待不是后台进程的索引构建完成。因此可以使用background属性进行设置后台索引创建,操作如下:db.people.createIndex( { zipcode: 1 }, { background: true } ) 默认情况下,在创建索引时,background为false,可以和其他属性进行组合使用: db.people.createIndex( { zipcode: 1 }, { background: true, sparse: true } ) 复制代码2. 索引类型2.1 单字段索引(Single Field Indexes)MongoDB可以在任何一个字段中创建索引,默认情况下,所有的集合(collections)会在_id字段中创建索引。_id索引是为防止客户端插入具有相同value的_id字段的文档Document,而且不能删除_id字段索引。在分片群集中使用_id索引,如果不使用_id字段作为分片键,则应用程序必须确保_id字段中值的唯一性以防止出错,解决方法为使用标准的自动生成的ObjectId来完成。一般单字段索引的value中,“1”指定按升序对项目进行排序的索引,“-1”指定按降序对项目进行排序的索引。如下所示:在单个字段创建索引,示例如下:{ "_id": ObjectId("570c04a4ad233577f97dc459"), "score": 1034, "location": { state: "NY", city: "New York" } } //创建单字段索引 db.records.createIndex( { score: 1 } ) //支持的查询 db.records.find( { score: 2 } ) db.records.find( { score: { $gt: 10 } } ) 复制代码在嵌入式文档Document中的字段创建索引,示例如下:db.records.createIndex( { "location.state": 1 } ) //支持的查询 db.records.find( { "location.state": "CA" } ) db.records.find( { "location.city": "Albany", "location.state": "NY" } ) 复制代码在嵌入式文档Document创建索引,示例如下:db.records.createIndex( { location: 1 } ) //支持查询 db.records.find( { location: { city: "New York", state: "NY" } } ) 复制代码2.2 复合索引(Compound Index)复合索引指的是将多个key组合到一起创建索引,这样可以加速匹配多个键的查询。特性如下:MongoDB对任何复合索引都限制了32个字段;无法创建具有散列索引(hash index)类型的复合索引。如果尝试创建包含散列索引字段的复合索引,则会报错;复合索引创建字段索引的顺序是很重要的。因为索引以升序(1)或降序(-1)排序顺序存储对字段的引用; 对于单字段索引,键的排序顺序无关紧要,因为MongoDB可以在任一方向上遍历索引。 但是,对于复合索引,排序顺序可以决定索引是否可以支持排序操作;除了支持在所有索引字段上匹配的查询之外,复合索引还可以支持与索引字段的前缀匹配的查询。创建复合索引的格式:db.collection.createIndex( { <field1>: <type>, <field2>: <type2>, ... } ) 复制代码排序顺序,两个字段的复合索引示例,index{userid:1,score:-1},先userid的value排序,然后再userid排序基础下进行score排序。如下图:创建复合索引,示例如下:{ "_id": ObjectId(...), "item": "Banana", "category": ["food", "produce", "grocery"], "location": "4th Street Store", "stock": 4, "type": "cases" } //创建复合索引 db.products.createIndex( { "item": 1, "stock": 1 } ) //支持的查询 db.products.find( { item: "Banana" } ) db.products.find( { item: "Banana", stock: { $gt: 5 } } ) 复制代码复合索引中的前缀查询,示例如下://创建复合索引 db.products.createIndex({ "item": 1, "location": 1, "stock": 1 }) //前缀为:{ item: 1 }与{ item: 1, location: 1 } //支持前缀查询为 db.products.find( { item: "Banana" } ) db.products.find( { item: "Banana", location: “beijing”} ) //不支持前缀查询,不会提高查询效率 //不包含前缀字段 db.products.find( { location: “beijing”} ) db.products.find( { stock: { $gt: 5 } ) db.products.find( { location: “beijing”,stock: { $gt: 5 } ) //不按照创建复合索引字段顺序的前缀查询 db.products.find( { location: “beijing”,item: "Banana" },stock: { $gt: 5 } ) 复制代码2.3 多键索引MongoDB使用多键索引为数组的每个元素都创建索引,多键索引可以建立在字符串、数字等key或者内嵌文档(document)的数组上,如果索引字段包含数组值,MongoDB会自动确定是否创建多键索引; 您不需要手动指定多键类型。 其中创建方式:db.coll.createIndex( { <field>: < 1 or -1 > } ) 复制代码索引边界使用多键索引,会出现索引边界(索引边界即是查询过程中索引能查找的范围)的计算,并计算必须遵循一些规则。即当多个查询的条件中字段都存在索引中时,MongoDB将会使用交集或者并集等来判断这些条件索引字段的边界最终产生一个最小的查找范围。可以分情况:1).交集边界交集边界即为多个边界的逻辑交集,对于给定的数组字段,假定一个查询使用了数组的多个条件字段并且可以使用多键索引。如果使用了$elemMatch连接了条件字段,则MongoDB将会相交多键索引边界,示例如下://survey Collection中document有一个item字段和一个ratings数组字段 { _id: 1, item: "ABC", ratings: [ 2, 9 ] } { _id: 2, item: "XYZ", ratings: [ 4, 3 ] } //在ratings数组上创建多键索引: db.survey.createIndex({ratings:1}) //两种查询 db.survey.find({ratings:{$elemMatch:{$gte:3,$lte:6}}}) //(1) db.survey.find( { ratings : { $gte: 3, $lte: 6 } } ) //(2) 复制代码查询条件分别为大于等于3、小于等于6,其中 (1)中使用了$elemMatch连接查询条件,会产生一个交集ratings:[[3,6]。在(2)查询中,没使用$elemMatch,则不会产生交集,只要满足任何一个条件即可。2).并集边界并集边界常常用在确定多键组合索引的边界,例如:给定的组合索引{a:1,b:1},在字段a上有一个边界:[3,+∞),在字段b上有一个边界:(-∞,6],相并这两个边界的结果是:{ a: [ [ 3, Infinity ] ], b: [ [ -Infinity, 6 ] ] }。而且如果MongoDB没法并集这两个边界,MongoDB将会强制使用索引的第一个字段的边界来进行索引扫描,在这种情况下就是: a: [ [ 3, Infinity ] ]。3、数组字段的组合索引一个组合索引的索引字段是数组,例如一个survey collection集合document文档中含有item字段和ratings数组字段,示例如下:{ _id: 1, item: "ABC", ratings: [ 2, 9 ] } { _id: 2, item: "XYZ", ratings: [ 4, 3 ] } //在item字段和ratings字段创建一个组合索引: db.survey.createIndex( { item: 1, ratings: 1 } ) //查询条件索引包含的两个key db.survey.find( { item: "XYZ", ratings: { $gte: 3 } } ) 复制代码分别处理查询条件:item: "XYZ" --> [ [ "XYZ", "XYZ" ] ]; ratings: { $gte: 3 } --> [ [ 3, Infinity ] ]. 复制代码MongoDB使用并集边界来组合这两个边界:{ item: [ [ "XYZ", "XYZ" ] ], ratings: [ [ 3, Infinity ] ] } 复制代码4).内嵌文档document的数组上建立组合索引如果数组中包含内嵌文档document,想在包含的内嵌文档document字段上建立索引,需要在索引声明中使用逗号“,” 来分隔字段名,示例如下:ratings: [ { score: 2, by: "mn" }, { score: 9, by: "anon" } ] 复制代码则score字段名称就是:ratings.score。5).混合不是数组类型的字段和数组类型字段的并集{ _id: 1, item: "ABC", ratings: [ { score: 2, by: "mn" }, { score: 9, by: "anon" } ] } { _id: 2, item: "XYZ", ratings: [ { score: 5, by: "anon" }, { score: 7, by: "wv" } ] } //在item和数组字段ratings.score和ratings.by上创建一个组合索引 db.survey2.createIndex( { "item": 1, "ratings.score": 1, "ratings.by": 1 } ) //查询 db.survey2.find( { item: "XYZ", "ratings.score": { $lte: 5 }, "ratings.by": "anon" } ) 复制代码分别对查询条件进行处理:item: "XYZ"--> [ ["XYZ","XYZ"] ]; score: {$lte:5}--> [[-Infinity,5]]; by: "anon" -->["anon","anon"]. 复制代码MongoDB可以组合 item键的边界与 ratings.score和ratings.by两个边界中的一个,到底是score还是by索引边界这取决于查询条件和索引键的值。MongoDB不能确保哪个边界和item字段进行并集。 但如果想组合ratings.score和ratings.by边界,则查询必须使用$elemMatch。6).数组字段索引的并集边界在数组内部并集索引键的边界,除了字段名称外,索引键必须有相同的字段路径,查询的时候必须在路径上使用$elemMatch进行声明对于内嵌的文档,使用逗号分隔的路径,比如a.b.c.d是字段d的路径。为了在相同的数组上并集索引键的边界,需要$elemMatch必须使用在a.b.c的路径上。比如:在ratings.score和ratings.by字段上创建组合索引:db.survey2.createIndex( { "ratings.score": 1, "ratings.by": 1 } ) 复制代码字段ratings.score和ratings.by拥有共同的路径ratings。下面的查询使用$elemMatch则要求ratings字段必须包含一个元素匹配这两个条件:db.survey2.find( { ratings: { $elemMatch: { score: { $lte: 5 }, by: "anon" } } } ) 复制代码分别对查询条件进行处理:score: { $lte: 5 } --> [ -Infinity, 5 ]; by: "anon"--> [ "anon", "anon" ]. 复制代码MongoDB可以使用并集边界来组合这两个边界:{ "ratings.score" : [ [ -Infinity, 5 ] ], "ratings.by" : [ [ "anon", "anon" ] ] } 复制代码7). 还有不使用$elemMatch进行查询以及不完整的路径上使用$elemMatch,想要了解更多可以查看《官方文档-Multikey Index Bounds》。限制:对于一个组合多键索引,每个索引文档最多只能有一个索引字段的值是数组。如果组合多键索引已经存在了,不能在插入文档的时候违反这个限制;不能声明一个多键索引作为分片键索引;哈希索引不能拥有多键索引;多键索引不能进行覆盖查询;当一个查询声明把数组整体作为精确匹配的时候,MongoDB可以使用多键索引来查找这个查询数组的第一个元素,但是不能使用多键索引扫描来找出整个数组。代替方案是当使用多键索引查询出数组的第一个元素之后,MongoDB再对过滤之后的文档再进行一次数组匹配。2.4 全文索引(text index)MongoDB提供了一种全文索引类型,支持在Collection中搜索字符串内容,对字符串与字符串数组创建全文可搜索的索引。 这些全文索引不存储特定于语言的停用词(例如“the”,“a”,“或”),并且阻止document集合中的单词仅存储根词。创建方式如下:db.collection.createIndex( { key: "text",key:"text" ..... } ) 复制代码而且MongoDB提供权重以及通配符的创建方式。查询方式多个字符串空格隔开,排除查询使用“-”如下所示:db.collection.find({$text:{$search:"runoob add -cc"}}) 复制代码要删除全本索引,需要将索引的名称传递给db.collection.dropIndex()方法, 而要获取索引的名称,使用db.collection.getIndexes()方法。还可以指定全文索引的语言,通过default_language属性 在创建时指定, 或者使用language_override属性 覆盖掉创建document文档时默认的语言,如下所示://指定不同的语言的方法:创建全文索引的时候使用default_language属性 db.collection.createIndex( { content : "text" }, { default_language: "spanish" }) //使用language_override属性覆盖默认的语言 db.quotes.createIndex( { quote : "text" }, { language_override: "idioma" } ) //默认的全文索引名称为context_text,users.comments.text,指定名称MyTextIndex db.collection.createIndex( { content: "text", "users.comments": "text", "users.profiles": "text" }, { name: "MyTextIndex" } ) 复制代码权重每个全文索引可以通过设置权重来分配不同的搜索程度,默认权重为1,对于文档中的每个索引字段,MongoDB将匹配数乘以权重并将结果相加。 使用此总和,MongoDB然后计算文档的分数,示例如下:{ _id: 1, content: "This morning I had a cup of coffee.", about: "beverage", keywords: [ "coffee" ] } { _id: 2, content: "Who doesn't like cake?", about: "food", keywords: [ "cake", "food", "dessert" ] } //通过db.blog.createIndex来指定weight权重 db.blog.createIndex( { content: "text", keywords: "text", about: "text" }, { weights: { content: 10, keywords: 5 }, name: "TextIndex" } ) 复制代码content权重为10,keywords为5,about为默认权重1,因此可以得出content对于keywords查询频率高于2倍,而对于about字段则是10倍。通配符全文索引在多个字段上创建全文索引时,还可以使用通配符说明符($**)。 使用通配符全文索引,MongoDB会为包含Collection中每个Document的字符串数据。例如:db.collection.createIndex( { "$**": "text" } ) 复制代码通配符全本索引是多个字段上的全本索引。 因此,可以在创建索引期间为特定字段指定权重,以控制结果的排名。限制每个Collection一个全文索引:一个collection最多只有一个全文索引,Text Search 和Hints函数,如果查询包含$ text查询表达式,则不能使用hint();Text Index and Sort,排序操作无法从文本索引获取排序顺序,即使是复合文本索引也是如此; 即排序操作不能使用文本索引中的排序;复合索引:复合索引可以包括文本索引键与升序/降序索引键的组合。 但是,这些复合索引具有以下限制:1).复合文本索引不能包含任何其他特殊索引类型,例如多键或地理空间索引字段。2).如果复合文本索引包括文本索引键之前的键,则执行$ text搜索时,查询谓词必须包含前面键上的相等匹配条件。3).创建复合文本索引时,必须在索引规范文档中相邻地列出所有文本索引键。2.5 Hash 索引散列索引使用散列函数来计算索引字段值的散列值。 散列函数会折叠嵌入的文档并计算整个值的散列值,但不支持多键(即数组)索引。 生成hash索引key使用了convertShardKeyToHashed()方法。创建方式如下:db.collection.createIndex( { _id: "hashed" } ) 复制代码而且散列索引支持使用散列分片键进行分片。 基于散列的分片使用字段的散列索引作为分片键来分割整个分片群集中的数据。3. 索引属性索引属性有TTL索引、惟一性索引、部分索引、稀疏索引以及区分大小写索引。3.1 TTL索引(TTL Indexes)TTL索引是特殊的单字段索引,并且字段类型必须是date类型或者包含有date类型的数组,MongoDB可以使用它在一定时间后或在特定时钟时间自动从集合中删除文档。 数据到期对于某些类型的信息非常有用,例如机器生成的事件数据,日志和会话信息,这些信息只需要在数据库中持续有限的时间。创建TTL索引方法,和普通索引的创建方法一样,只是会多加一个expireAfterSeconds的属性,格式如下:db.collection.createIndex( {key and index type specification},{ expireAfterSeconds: time}) 复制代码例子:db.eventlog.createIndex( { "lastModifiedDate": 1 }, { expireAfterSeconds: 3600 } ) 复制代码指定过期时间首先在保存BSON日期类型值或BSON日期类型对象数组的字段上创建TTL索引,并指定expireAfterSeconds值为0.对于集合中的每个文档,设置 索引日期字段为与文档到期时间对应的值。示例操作如下:第一步:db.log_events.createIndex( { "expireAt": 1 }, { expireAfterSeconds: 0 } ) 复制代码第二步:db.log_events.insert( { "expireAt": new Date('July 22, 2013 14:00:00'), "logEvent": 2, "logMessage": "Success!" } ) 复制代码数据过期类型:当指定时间到了过期的阈值数据就会过期并删除;如果字段是数组,并且索引中有多个日期值,MongoDB使用数组中最低(即最早)的日期值来计算到期阈值;如果文档(document)中的索引字段不是日期或包含日期值的数组,则文档(document)将不会过期;如果文档(document)不包含索引字段,则文档(document)不会过期。TTL索引特有限制:TTL索引是单字段索引。 复合索引不支持TTL并忽略expireAfterSeconds选项;_id属性不支持TTL索引;无法在上限集合上创建TTL索引,因为MongoDB无法从上限集合中删除文档;不能使用createIndex()方法来更改现有索引的expireAfterSeconds值。而是将collMod数据库命令与索引集合标志结合使用。 否则,要更改现有索引的选项的值,必须先删除索引并重新创建;如果字段已存在非TTL单字段索引,则无法在同一字段上创建TTL索引,因为无法在相同key创建不同类型的的索引。 要将非TTL单字段索引更改为TTL索引,必须先删除索引并使用expireAfterSeconds选项重新创建。3.2 惟一性索引(Unique Indexes)唯一索引可确保索引字段不存储重复值; 即强制索引字段的唯一性。 默认情况下,MongoDB在创建集合期间在_id字段上创建唯一索引。创建方式如下:db.collection.createIndex( <key and index type specification>, { unique: true } ) 复制代码单个字段创建方式,示例如下:db.members.createIndex( { "user_id": 1 }, { unique: true } ) 复制代码唯一性复合索引: 还可以对复合索引强制执行唯一约束。 如果对复合索引使用唯一约束,则MongoDB将对索引键值的组合强制实施唯一性。示例如下://创建的索引且强制groupNumber,lastname和firstname值组合的唯一性。 db.members.createIndex( { groupNumber: 1, lastname: 1, firstname: 1 }, { unique: true } ) 复制代码唯一多键索引:{ _id: 1, a: [ { loc: "A", qty: 5 }, { qty: 10 } ] } //创建索引: db.collection.createIndex( { "a.loc": 1, "a.qty": 1 }, { unique: true } ) //插入数据:唯一索引允许将以下Document插入Collection中,因为索引强制执行a.loc和a.qty值组合的唯一性: db.collection.insert( { _id: 2, a: [ { loc: "A" }, { qty: 5 } ] } ) db.collection.insert( { _id: 3, a: [ { loc: "A", qty: 10 } ] } ) 复制代码创建唯一索引到副本或者分片中:对于副本集和分片集群,使用滚动过程创建唯一索引需要在过程中停止对集合的所有写入。 如果在过程中无法停止对集合的所有写入,请不要使用滚动过程。 相反,通过以下方式在集合上构建唯一索引:在主服务器上为副本集发出db.collection.createIndex()在mongos上为分片群集发出db.collection.createIndex()NOTE:详细解析可以看限制:如果集合已经包含超出索引的唯一约束的数据(即有重复数据),则MongoDB无法在指定的索引字段上创建唯一索引。不能在hash索引上创建唯一索引唯一约束适用于Collection中的一个Document。由于约束适用于单文档document,因此对于唯一的多键索引,只要该文档document的索引键值不与另一个文档document的索引键值重复,文档就可能具有导致重复索引键值的数组元素。 在这种情况下,重复索引记录仅插入索引一次。分片Collection唯一索引只能如下:1).分片键上的索引2).分片键是前缀的复合索引3). 默认的_id索引; 但是,如果_id字段不是分片键或分片键的前缀,则_id索引仅对每个分片强制执行唯一性约束。如果_id字段不是分片键,也不是分片键的前缀,MongoDB希望应用程序在分片中强制执行_id值的唯一性。3.3 部分索引(Partial Indexes)部分索引通过指定的过滤表达式去达到局部搜索。通过db.collection.createIndex()方法中增加partialFilterExpression属性创建,过滤表达式如下:等式表达式(即 file:value或使用$eq运算符)$exists表达式$gt,$gte,$lt,$lte 表达式$type表达式$and示例如下://创建部分索引 db.restaurants.createIndex( { cuisine: 1, name: 1 }, { partialFilterExpression: { rating: { $gt: 5 } } } ) //查询情况分类 db.restaurants.find( { cuisine: "Italian", rating: { $gte: 8 } } ) //(1) db.restaurants.find( { cuisine: "Italian", rating: { $lt: 8 } } ) //(2) db.restaurants.find( { cuisine: "Italian" } ) //(3) 复制代码其中:(1)查询: 查询条件{ $gte: 8 }于创建索引条件{ $gt: 5 }可以构成一个完整集(查询条件是创建索引条件的子集,即大于5可以包含大于等于 8),可以使用部分索引查询。(2)查询: 条件达不到完整集,MongoDB将不会将部分索引用于查询或排序操作。(3)查询: 次查询没有使用过滤表达式,也不会使用部分索引,因为要使用部分索引,查询必须包含过滤器表达式(或指定过滤器表达式子集的已修改过滤器表达式)作为其查询条件的一部分限制:不可以仅通过过滤表达式创建多个局部索引;不可以同时使用局部索引和稀疏索引(sparse index);_id索引不能使用局部索引,分片索引(shard key index)也不能使用局部索引;同时指定partialFilterExpression和唯一约束,则唯一约束仅适用于满足过滤器表达式的文档。 如果Document不符合筛选条件,则具有唯一约束的部分索引是允许插入不符合唯一约束的Document。3.4 稀疏索引(Sparse Indexes)稀疏索只引搜索包含有索引字段的文档的条目,跳过索引键不存在的文档,即稀疏索引不会搜索不包含稀疏索引的文档。默认情况下, 2dsphere (version 2), 2d, geoHaystack, 全文索引等总是稀疏索引。创建方式db.collection.createIndex()方法增加sparse属性,如下所示:db.addresses.createIndex( { "xmpp_id": 1 }, { sparse: true } ) 复制代码稀疏索引不被使用的情况: 如果稀疏索引会导致查询和排序操作的结果集不完整,MongoDB将不会使用该索引,除非hint()示显式指定索引。稀疏复合索引:对于包含上升/下降排序的稀疏复合索引,只要复合索引中的一个key 索引存在都会被检测出来对于包含上升/下降排序的包含地理空间可以的稀疏复合索引,只有存在地理空间key才能被检测出来对于包含上升/下降排序的全文索引的稀疏复合索引,只有存在全文索引索引才可以被检测稀疏索引与唯一性: 一个既包含稀疏又包含唯一的索引避免集合上存在一些重复值得文档,但是允许多个文档忽略该键。满足稀疏索引和唯一性操作其两个限制都要遵循。整合示例如下:{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" } { "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 } { "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 } //在score中创建稀疏索引: db.scores.createIndex( { score: 1 } , { sparse: true } ) //查询 db.scores.find( { score: { $lt: 90 } } ) //(1) db.scores.find().sort( { score: -1 } ) //(2) db.scores.find().sort( { score: -1 } ).hint( { score: 1 } ) //(3) //在score字段上创建具有唯一约束和稀疏过滤器的索引: db.scores.createIndex( { score: 1 } , { sparse: true, unique: true } ) //该索引允许插入具有score字段的唯一值的文档或不包括得分字段的文档。如下: db.scores.insert( { "userid": "AAAAAAA", "score": 43 } ) db.scores.insert( { "userid": "BBBBBBB", "score": 34 } ) db.scores.insert( { "userid": "CCCCCCC" } ) db.scores.insert( { "userid": "DDDDDDD" } ) //索引不允许添加以下文档,因为已存在score值为82和90的文档 db.scores.insert( { "userid": "AAAAAAA", "score": 82 } ) db.scores.insert( { "userid": "BBBBBBB", "score": 90 } ) 复制代码其中:(1)查询: 可以使用稀疏索引查询,返回完整集:{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }(2)查询: 即使排序是通过索引字段进行的,MongoDB也不会选择稀疏索引来完成查询以返回完整的结果:{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }{ "_id" : ObjectId("523b6e32fb408eea0eec2647"), "userid" : "newbie" }(3)查询: 使用hint()返回所需完整集:{ "_id" : ObjectId("523b6e6ffb408eea0eec2649"), "userid" : "nina", "score" : 90 }{ "_id" : ObjectId("523b6e61fb408eea0eec2648"), "userid" : "abby", "score" : 82 }4. 其他事项4.1 索引策略索引策略:应用程序的最佳索引必须考虑许多因素,包括期望查询的类型,读取与写入的比率以及系统上的可用内存量。在开发索引策略时,您应该深入了解应用程序的查询。在构建索引之前,映射将要运行的查询类型,以便您可以构建引用这些字段的索引。索引具有性能成本,但是对于大型数据集上的频繁查询而言,它们的价值更高。考虑应用程序中每个查询的相对频率以及查询是否证明索引是合理的。设计索引的最佳总体策略是使用与您将在生产中运行的数据集类似的数据集来分析各种索引配置,以查看哪些配置性能最佳。检查为集合创建的当前索引,以确保它们支持您当前和计划的查询。如果不再使用索引,请删除索引。通常,MongoDB仅使用一个索引来完成大多数查询。但是,$或查询的每个子句可能使用不同的索引,从2.6开始,MongoDB可以使用多个索引的交集。4.2 后续后续还会有MongonDB索引优化,副本集以及分片总结、最重要还会总结在使用MongoDB实战以及实战过程出现的一些坑,可关注后续更新MongoDB系列。
MongoDB中聚合(aggregate) 操作将来自多个document的value组合在一起,并通过对分组数据进行各种操作处理,并返回计算后的数据结果,主要用于处理数据(诸如统计平均值,求和等)。MongoDB提供三种方式去执行聚合操作:聚合管道(aggregation pipeline)、Map-Reduce函数以及单一的聚合命令(count、distinct、group)。1. 聚合管道(aggregation pipeline)1.1聚合管道聚合管道是由aggregation framework将文档进入一个由多个阶段(stage)组成的管道,可以对每个阶段的管道进行分组、过滤等功能,然后经过一系列的处理,输出相应的聚合结果。如图所示:聚合管道操作:db.orders.aggregate([ { $match: { status: "A" } }, { $group: { _id: "$cust_id", total: { $sum: "$amount" } } } ]) 复制代码$match阶段:通过status字段过滤出符合条件的Document(即是Status等于“A”的Document);** $group 阶段:按cust_id字段对Document进行分组,以计算每个唯一cust_id的金额总和。**1.2 管道管道在Unix和Linux中一般用于将当前命令的输出结果作为下一个命令的参数,MongoDB的聚合管道将MongoDB文档在一个管道处理完毕后将结果传递给下一个管道处理。管道操作是可以重复的。最基本的管道功能提供过滤器filter,其操作类似于查询和文档转换,可以修改输出文档的形式。 其他管道操作提供了按特定字段或字段对文档进行分组和排序的工具,以及用于聚合数组内容(包括文档数组)的工具。 此外,管道阶段可以使用运算符执行任务,例如计算平均值或连接字符串。总结如下:管道操作符常用管道解析$group将collection中的document分组,可用于统计结果$match过滤数据,只输出符合结果的文档$project修改输入文档的结构(例如重命名,增加、删除字段,创建结算结果等)$sort将结果进行排序后输出$limit限制管道输出的结果个数$skip跳过制定数量的结果,并且返回剩下的结果$unwind将数组类型的字段进行拆分表达式操作符常用表达式含义$sum计算总和,{$sum: 1}表示返回总和×1的值(即总和的数量),使用{$sum: '$制定字段'}也能直接获取制定字段的值的总和$avg求平均值$min求min值$max求max值$push将结果文档中插入值到一个数组中$first根据文档的排序获取第一个文档数据$last同理,获取最后一个数据为了便于理解,将常见的mongo的聚合操作和MySql的查询做类比:MongoDB聚合操作MySql操作/函数$matchwhere$groupgroup by$matchhaving$projectselect$sortorder by$limitlimit $sumsum()$lookupjoin1.3 Aggregation Pipeline 优化聚合管道可以确定它是否仅需要文档中的字段的子集来获得结果。 如果是这样,管道将只使用那些必需的字段,减少通过管道的数据量管道序列优化化管道序列优化化:1).使用$projector/$addFields+$match 序列优化:当Aggregation Pipeline中有多个$projectior/$addFields阶段和$match 阶段时,会先执行有依赖的$projector/$addFields阶段,然后会新创建的$match阶段执行,如下,{ $addFields: { maxTime: { $max: "$times" }, minTime: { $min: "$times" } } }, { $project: { _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1, avgTime: { $avg: ["$maxTime", "$minTime"] } } }, { $match: { name: "Joe Schmoe", maxTime: { $lt: 20 }, minTime: { $gt: 5 }, avgTime: { $gt: 7 } } } 复制代码优化执行:{ $match: { name: "Joe Schmoe" } }, { $addFields: { maxTime: { $max: "$times" }, minTime: { $min: "$times" } } }, { $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } }, { $project: { _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1, avgTime: { $avg: ["$maxTime", "$minTime"] } } }, { $match: { avgTime: { $gt: 7 } } } 复制代码2). $sort + $match 以及$project + $skip,当$sort/$project跟在$match/$skip之后时,会先执行$match/$skip后再执行$sort/$project,$sort以达到最小化需排列的对象数,$skip约束,如下:{ $sort: { age : -1 } }, { $match: { score: 'A' } } { $project: { status: 1, name: 1 } }, { $skip: 5 } 复制代码优化执行:{ $match: { score: 'A' } }, { $sort: { age : -1 } } { $skip: 5 }, { $project: { status: 1, name: 1 } } 复制代码3). $redact+$match序列优化,当$redact后有$match时,可能会新创一个$match阶段进行优化,如下,{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "?PRUNE", else: "?DESCEND" } } }, { $match: { year: 2014, category: { $ne: "Z" } } } 复制代码优化执行:{ $match: { year: 2014 } }, { $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "?PRUNE", else: "?DESCEND" } } }, { $match: { year: 2014, category: { $ne: "Z" } } } 复制代码还有很多管道序列优化可以查看《官方文档-Aggregation Pipeline Optimization》。1.4 Aggregation Pipeline以及分片(Sharded)collections如果管道以$match精确分片 key开始的后,所有管道会在匹配的分片上进行。对于需运行在多分片中的聚合(aggregation)操作,如果不不需要在主分片进行的,这些操作后的结果会路由到随机分片中进行合并结果,避免重载该主分片的数据库。$out和$look阶段必须在主分片数据库运行。2. Map-Reduce函数MongoDB还提供map-reduce操作来执行聚合。 通常,map-reduce操作有两个阶段:一个map阶段,它处理每个文档并为每个输入文档发出一个或多个对象,以及reduce阶段组合map操作的输出。 可选地,map-reduce可以具有最终化阶段以对结果进行最终修改。 与其他聚合操作一样,map-reduce可以指定查询条件以选择输入文档以及排序和限制结果。Map-reduce使用自定义JavaScript函数来执行映射和减少操作,以及可选的finalize操作。 虽然自定义JavaScript与聚合管道相比提供了极大的灵活性,但通常,map-reduce比聚合管道效率更低,更复杂。模式如下:3. 单一的聚合命令MongoDB还提供了,db.collection.estimatedDocumentCount(),db.collection.count()和db.collection.distinct() 所有这些单一的聚合命令。 虽然这些操作提供了对常见聚合过程的简单访问操作,但它们缺乏聚合管道和map-reduce的灵活性和功能。模型如下总结可使用MongoDB中聚合操作用于数据处理,可以适应于一些数据分析等,聚合的典型应用包括销售数据的业务报表,比如将各地区的数据分组后计算销售总和、财务报表等。最后想要更加深入理解还需要自己去实践。
1. 权限管理相关概念权限管理是一个几乎所有后台系统的都会涉及的一个重要组成部分,主要目的是对整个后台管理系统进行权限的控制。常见的基于角色的访问控制,其授权模型为“用户-角色-权限”,简明的说,一个用户拥有多个角色,一个角色拥有多个权限。其中,用户: 不用多讲,大家也知道了;角色: 一个集合的概念,角色管理是确定角色具备哪些权限的一个过程 ;权限:1).页面权限,控制你可以看到哪个页面,看不到哪个页面; 2). 操作权限,控制你可以在页面上进行哪些操作(查询、删除、编辑等); 3).数据权限,是控制你可以看到哪些数据。实质是: 权限(Permission) = 资源(Resource) + 操作(Privilege) 角色(Role) = 权限的集合(a set of low-level permissions) 用户(User) = 角色的集合(high-level roles)权限管理过程:鉴权管理,即权限判断逻辑,如菜单管理(普通业务人员登录系统后,是看不到【用户管理】菜单的)、功能权限管理(URL访问的管理)、行级权限管理等授权管理,即权限分配过程,如直接对用户授权,直接分配到用户的权限具有最优先级别、对用户所属岗位授权,用户所属岗位信息可以看作是一个分组,和角色的作用一样,但是每个用户只能关联一个岗位信息等。在实际项目中用户数量多,逐一的为每个系统用户授权,这是极其繁琐的事,所以可以学习linux文件管理系统一样,设置group模式,一组有多个用户,可以为用户组授权相同的权限,简便多了。这样模式下:每个用户的所有权限=用户个人的权限+用户组所用的权限用户组、用户、与角色三者关系如下:再结合权限管理的页面权限、操作权限,如菜单的访问、功能模块的操作、按钮的操作等等,可把功能操作与资源统一管理,即让它们直接与权限关联起来,关系图如下:2. 授权过程分析2.1 授权访问权限工作流程:FilterSecurityInterceptor doFilter()->invoke() ->AbstractSecurityInterceptor beforeInvocation() ->SecurityMetadataSource 获取ConfigAttribute属性信息(从数据库或者其他数据源地方) getAttributes() ->AccessDecisionManager() 基于AccessDecisionVoter实现授权访问 Decide() ->AccessDecisionVoter 受AccessDecisionManager委托实现授权访问 vote() 复制代码默认授权过程会使用这样的工作流程,接下来来分析各个组件的功能与源码。2.2 AbstractSecurityInterceptor分析FilterSecurityInterceptor为授权拦截器, 在FilterSecurityInterceptor中有一个封装了过滤链、request以及response的FilterInvocation对象进行操作,在FilterSecurityInterceptor,主要由invoke()调用其父类AbstractSecurityInterceptor的方法。invoke()分析:public void invoke(FilterInvocation fi) throws IOException, ServletException { ..... // 获取accessDecisionManager权限决策后结果状态、以及权限属性 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } } 复制代码AbstractSecurityInterceptor 的授权过滤器主要方法beforeInvocation(),afterInvocation()以及authenticateIfRequired(),其最主要的方法beforeInvocation() 分析如下:protected InterceptorStatusToken beforeInvocation(Object object) { .... //从SecurityMetadataSource的权限属性 Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); if (attributes == null || attributes.isEmpty()) { ..... publishEvent(new PublicInvocationEvent(object)); return null; // no further work post-invocation } //调用认证环节获取authenticated(包含用户的详细信息) Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //进行关键的一步:授权的最终决策 this.accessDecisionManager.decide(authenticated, object, attributes); } catch (AccessDeniedException accessDeniedException) { publishEvent(new AuthorizationFailureEvent(object, attributes, authenticated, accessDeniedException)); throw accessDeniedException; } // Attempt to run as a different user Authentication runAs = this.runAsManager.buildRunAs(authenticated, object, attributes); if (runAs == null) { if (debug) { logger.debug("RunAsManager did not change Authentication object"); } // no further work post-invocation return new InterceptorStatusToken(SecurityContextHolder.getContext(), false, attributes, object); } else { if (debug) { logger.debug("Switching to RunAs Authentication: " + runAs); } SecurityContext origCtx = SecurityContextHolder.getContext(); SecurityContextHolder.setContext(SecurityContextHolder.createEmptyContext()); SecurityContextHolder.getContext().setAuthentication(runAs); // need to revert to token.Authenticated post-invocation return new InterceptorStatusToken(origCtx, true, attributes, object); } } 复制代码2.3 SecurityMetadataSourceSecurityMetadataSource是从数据库或者其他数据源中加载ConfigAttribute,为了在AccessDecisionManager.decide() 最终决策中进行match。其有三个方法:Collection<ConfigAttribute> getAttributes(Object var1) throws IllegalArgumentException;//加载权限资源 Collection<ConfigAttribute> getAllConfigAttributes();//加载所有权限资源 boolean supports(Class<?> var1); 复制代码2.4 AccessDecisionManagerAccessDecisionManager被AbstractSecurityInterceptor 拦截器调用进行最终访问控制决策。 而且由AuthenticationManager创建的Authentication object中的GrantedAuthority,首先被授权模块中的 AccessDecisionManager读取使用,当复杂的GrantedAuthority,getAuthority()为null,因此需要AccessDecisionManager专门支持GrantedAuthority实现以便了解其内容。AccessDecisionManager接口方法:void decide(Authentication authentication, Object secureObject, Collection<ConfigAttribute> attrs) throws AccessDeniedException; boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 复制代码2.5 AccessDecisionVoterAccessDecisionManager.decide()将使用AccessDecisionVoter进行投票决策。AccessDecisionVoter进行投票访问控制决策,访问不通过就抛出AccessDeniedException。** AccessDecisionVoter**接口方法:int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attrs); boolean supports(ConfigAttribute attribute); boolean supports(Class clazz); 复制代码AccessDecisionVoter的核心方法vote() 通常是获取Authentication的GrantedAuthority与已定义好的ConfigAttributes进行match,如果成功为投同意票,匹配不成功为拒绝票,当ConfigAttributes中无属性时,才投弃票。Spring Security提供了三种投票方式去实现AccessDecisionManager接口进行投票访问控制决策:ConsensusBased: 大多数voter同意访问就授权访问AffirmativeBased: 只要一个以上voter同意访问就授权访问,全部UnanimousBased : 只有全体同意了才授权访问且AccessDecisionVoter用三个静态变量表示voter投票情况:ACCESS_ABSTAIN: 弃权ACCESS_DENIED: 拒绝访问ACCESS_GRANTED: 允许访问Note: 当所有voter都弃权时使用变量allowIfEqualGrantedDeniedDecisions来判断,true为通过,false抛出AccessDeniedException。此外可自定义AccessDecisionManager实现接口,因为可能某些AccessDecisionVoter具有权重比高投票权或者某些AccessDecisionVoter具有一票否定权。AccessDecisionVoter的Spring security实现类RoleVoter和AuthenticatedVoter。RoleVoter为最为常见的AccessDecisionVoter,其为简单的权限表示,并以前缀为ROLE_,vote匹配规则也跟上面一样。源码分析:Public int vote(Authentication authentication,Object object,Collection<ConfigAttribute>attributes){ //用户传递的authentication为null,拒绝访问 if(authentication==null){ return ACCESS_DENIED; } int result=ACCESS_ABSTAIN; Collection<?extendsGrantedAuthority>authorities=extractAuthorities(authentication); //依次进行投票 for(ConfigAttributeattribute:attributes){ if(this.supports(attribute)){ result=ACCESS_DENIED; //Attempt to find a matching granted authority for(GrantedAuthorityauthority:authorities){ if(attribute.getAttribute().equals(authority.getAuthority())){ returnACCESS_GRANTED; } } } } 复制代码3. 案例-自定义组件自定义组件:自定义FilterSecurityInterceptor,可仿写FilterSecurityInterceptor,实现抽象类AbstractSecurityInterceptor以及Filter接口,其主要的是把自定义的SecurityMetadataSource与自定义accessDecisionManager配置到自定义FilterSecurityInterceptor的拦截器中自定义SecurityMetadataSource,实现接口FilterInvocationSecurityMetadataSource,实现从数据库或者其他数据源中加载ConfigAttribute(即是从数据库或者其他数据源中加载资源权限)自定义accessDecisionManager,可使用基于AccessDecisionVoter实现权限认证的官方UnanimousBased自定义AccessDecisionVoter3.1 自定义MyFilterSecurityInterceptor自定义MyFilterSecurityInterceptor主要工作为:加载自定义的SecurityMetadataSource到自定义的FilterSecurityInterceptor中;加载自定义的AccessDecisionManager到自定义的FilterSecurityInterceptor中;重写invoke方法@Component public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { private FilterInvocationSecurityMetadataSource securityMetadataSource; @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain); invoke(fi); } private void invoke(FilterInvocation fi) throws IOException, ServletException { //fi里面有一个被拦截的url //里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限 //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够 InterceptorStatusToken token = super.beforeInvocation(fi); try { //执行下一个拦截器 fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.afterInvocation(token, null); } } @Override public void destroy() { } @Override public Class<?> getSecureObjectClass() { return null; } @Override public SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource; } public FilterInvocationSecurityMetadataSource getSecurityMetadataSource() { return this.securityMetadataSource; } //设置自定义的FilterInvocationSecurityMetadataSource @Autowired public void setSecurityMetadataSource(MyFilterInvocationSecurityMetadataSource messageSource) { this.securityMetadataSource = messageSource; } //设置自定义的AccessDecisionManager @Override @Autowired public void setAccessDecisionManager(AccessDecisionManager accessDecisionManager) { super.setAccessDecisionManager(accessDecisionManager); } } 复制代码3.2 自定义MyFilterInvocationSecurityMetadataSource自定义MyFilterInvocationSecurityMetadataSource主要工作为:从数据源中加载ConfigAttribute到SecurityMetadataSource资源器中重写getAttributes()加载ConfigAttribute为AccessDecisionManager.decide()授权决策做准备。@Component public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { private Map<String, Collection<ConfigAttribute>> configAttubuteMap = null; private void loadResourceDefine() { //todo 加载数据库的所有权限 Collection<ConfigAttribute> attributes; } @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { AntPathRequestMatcher matcher; String resUrl; HttpServletRequest request = ((FilterInvocation) object).getRequest(); //1.加载权限资源数据 if (configAttubuteMap == null) { loadResourceDefine(); } Iterator<String> iterator = configAttubuteMap.keySet().iterator(); while (iterator.hasNext()) { resUrl = iterator.next(); matcher = new AntPathRequestMatcher(resUrl); if (matcher.matches(request)) { return configAttubuteMap.get(resUrl); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } } 复制代码3.3 自定义MyAccessDecisionManager自定义MyAccessDecisionManager主要工作为:重写最终授权决策decide(),自定义授权访问策略@Component public class MyAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { ConfigAttribute c; String needRole; if(null== configAttributes || configAttributes.size() <=0) { return; } //1.获取已定义的好资源权限配置 Iterator<ConfigAttribute> iterable=configAttributes.iterator(); while (iterable.hasNext()){ c=iterable.next(); needRole=c.getAttribute(); //2.依次比对用户角色对应的资源权限 for (GrantedAuthority grantedAuthority:authentication.getAuthorities()){ if(needRole.trim().equals(grantedAuthority.getAuthority())){ return; } } } } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } 复制代码3.4 配置SecurityConfig配置SecurityConfig主要工作为:将FilterSecurityInterceptor拦截器加载WebSecurityConfig中protected void configure(HttpSecurity http) throws Exception { http.headers().frameOptions().disable().and() //表单登录 .formLogin() .loginPage(SecurityConstants.APP_FORM_LOGIN_PAGE) .loginProcessingUrl(SecurityConstants.APP_FORM_LOGIN_URL) .successHandler(authenticationSuccessHandler()) .failureHandler(authenticationFailureHandler()) .and() //应用sms认证配置 .apply(smsAuthenticationSecurityConfig) .and() //允许通过 .authorizeRequests() .antMatchers(SecurityConstants.APP_MOBILE_VERIFY_CODE_URL, SecurityConstants.APP_USER_REGISTER_URL, SecurityConstants.APP_FORM_LOGIN_INDEX_URL) .permitAll()//以上的请求都不需要认证 .and() //“记住我”配置 .rememberMe() .tokenRepository(jdbcTokenRepository())//token入库处理类 .tokenValiditySeconds(SecurityConstants.REMEMBER_ME_VERIFY_TIME)//remember-me有效时间设置 .rememberMeParameter(SecurityConstants.REMEMBER_ME_PARAM_NAME)//请求参数名设置 .and() .csrf().disable(); //增加自定义权限授权拦截器 http.addFilterBefore(myFilterSecurityInterceptor,FilterSecurityInterceptor.class); } 复制代码总结Spring Security授权过程中,可以会涉主要涉及了上面上面所述的组件,其中主要的还是跟着源码多跑几遍,了解其中的原理,才能更加流畅的码代码。到此为止写完Spring Security的认证和授权分析流程,接下来会结合前面小节,写一个Spring security完美的权限管理系统。各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
在认证过程和访问授权前必须了解spring Security如何知道我们要求所有用户都经过身份验证? Spring Security如何知道我们想要支持基于表单的身份验证?因此必须了解WebSecurityConfigurerAdapter配置类如何工作的。而且也必须了解清楚filter的顺序,才能更好了解其调用工作流程。1. WebSecurityConfigurerAdapter在使用WebSecurityConfigurerAdapter前,先了解Spring security config。Spring security config具有三个模块,一共有3个builder,认证相关的AuthenticationManagerBuilder和web相关的WebSecurity、HttpSecurity。AuthenticationManagerBuilder:用来配置全局的认证相关的信息,其实就是AuthenticationProvider和UserDetailsService,前者是认证服务提供商,后者是用户详情查询服务;WebSecurity: 全局请求忽略规则配置(比如说静态文件,比如说注册页面)、全局HttpFirewall配置、是否debug配置、全局SecurityFilterChain配置、privilegeEvaluator、expressionHandler、securityInterceptor;HttpSecurity:具体的权限控制规则配置。一个这个配置相当于xml配置中的一个标签。各种具体的认证机制的相关配置,OpenIDLoginConfigurer、AnonymousConfigurer、FormLoginConfigurer、HttpBasicConfigurer等。WebSecurityConfigurerAdapter提供了简洁方式来创建WebSecurityConfigurer,其作为基类,可通过实现该类自定义配置类,主要重写这三个方法:protected void configure(AuthenticationManagerBuilder auth) throws Exception {} public void configure(WebSecurity web) throws Exception {} protected void configure(HttpSecurity httpSecurity) throws Exception {} 复制代码而且其自动从SpringFactoriesLoader查找AbstractHttpConfigurer让我们去扩展,想要实现必须创建一个AbstractHttpConfigurer的扩展类,并在classpath路径下创建一个文件META-INF/spring.factories。例如:org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer其源码分析://1.init初始化:获取HttpSecurity和配置FilterSecurityInterceptor拦截器到WebSecurity public void init(final WebSecurity web) throws Exception { //获取HttpSecurity final HttpSecurity http = getHttp(); //配置FilterSecurityInterceptor拦截器到WebSecurity web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() { public void run() { FilterSecurityInterceptor securityInterceptor = http .getSharedObject(FilterSecurityInterceptor.class); web.securityInterceptor(securityInterceptor); } }); } ...... //2.获取HttpSecurity的过程 protected final HttpSecurity getHttp() throws Exception { if (http != null) { return http; } DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor .postProcess(new DefaultAuthenticationEventPublisher()); localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager(); authenticationBuilder.parentAuthenticationManager(authenticationManager); Map<Class<? extends Object>, Object> sharedObjects = createSharedObjects(); http = new HttpSecurity(objectPostProcessor, authenticationBuilder, sharedObjects); if (!disableDefaults) { // 默认的HttpSecurity的配置 http //添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用,禁用csrf().disable() .csrf().and() //添加WebAsyncManagerIntegrationFilter .addFilter(new WebAsyncManagerIntegrationFilter()) //允许配置异常处理 .exceptionHandling().and() //将安全标头添加到响应 .headers().and() //允许配置会话管理 .sessionManagement().and() //HttpServletRequest之间的SecurityContextHolder创建securityContext管理 .securityContext().and() //允许配置请求缓存 .requestCache().and() //允许配置匿名用户 .anonymous().and() //HttpServletRequestd的方法和属性注册在SecurityContext中 .servletApi().and() //使用默认登录页面 .apply(new DefaultLoginPageConfigurer<>()).and() //提供注销支持 .logout(); // @formatter:on ClassLoader classLoader = this.context.getClassLoader(); List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for(AbstractHttpConfigurer configurer : defaultHttpConfigurers) { http.apply(configurer); } } configure(http); return http; } ... //3.可重写方法实现自定义的HttpSecurity protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } .... 复制代码从源码init初始化模块中的“获取HttpSecurity”和“配置FilterSecurityInterceptor拦截器到WebSecurity”中可以看出,想要spring Security如何知道我们要求所有用户都经过身份验证? Spring Security如何知道我们想要支持基于表单的身份验证?只要重写protected void configure(HttpSecurity http) throws Exception方法即可。因此我们需要理解HttpSecurity的方法的作用,如何进行配置。下一节来讨论HttpSecurity。2. HttpSecurityHttpSecurity基于Web的安全性允许为特定的http请求进行配置。其有很多方法,列举一些常用的如下表:方法说明使用案例csrf()添加 CSRF 支持,使用WebSecurityConfigurerAdapter时,默认启用禁用:csrf().disable()openidLogin()用于基于 OpenId 的验证openidLogin().permitAll();authorizeRequests()开启使用HttpServletRequest请求的访问限制authorizeRequests().anyRequest().authenticated()formLogin()开启表单的身份验证,如果未指定FormLoginConfigurer#loginPage(String),则将生成默认登录页面formLogin().loginPage("/authentication/login").failureUrl("/authentication/login?failed")oauth2Login()开启OAuth 2.0或OpenID Connect 1.0身份验证authorizeRequests()..anyRequest().authenticated()..and().oauth2Login()rememberMe()开启配置“记住我”的验证authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin().permitAll().and().rememberMe()addFilter()添加自定义的filteraddFilter(new CustomFilter())addFilterAt()在指定filter相同位置上添加自定义filteraddFilterAt(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)addFilterAfter()在指定filter位置后添加自定义filteraddFilterAfter(new CustomFilter(), UsernamePasswordAuthenticationFilter.class)requestMatchers()开启配置HttpSecurity,仅当RequestMatcher相匹配时开启requestMatchers().antMatchers("/api/**")antMatchers()其可以与authorizeRequests()、RequestMatcher匹配,如:requestMatchers().antMatchers("/api/**")logout()添加退出登录支持。当使用WebSecurityConfigurerAdapter时,这将自动应用。默认情况是,访问URL”/ logout”,使HTTP Session无效来清除用户,清除已配置的任何#rememberMe()身份验证,清除SecurityContextHolder,然后重定向到”/login?success”logout().deleteCookies("remove").invalidateHttpSession(false).logoutUrl("/custom-logout").logoutSuccessUrl("/logout-success");HttpSecurity还有很多方法供我们使用,去配置HttpSecurity。由于太多这边就不一一说明,有兴趣可去研究。3. WebSecurityConfigurerAdapter使用WebSecurityConfigurerAdapter示例:@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyFilterSecurityInterceptor myFilterSecurityInterceptor; protected void configure(HttpSecurity http) throws Exception { http //request 设置 .authorizeRequests() //http.authorizeRequests() 方法中的自定义匹配 .antMatchers("/resources/**", "/signup", "/about").permitAll() // 指定所有用户进行访问指定的url .antMatchers("/admin/**").hasRole("ADMIN") //指定具有特定权限的用户才能访问特定目录,hasRole()方法指定用户权限,且不需前缀 “ROLE_“ .antMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")// .anyRequest().authenticated() //任何请求没匹配的都需要进行验证 .and() //login设置 自定义登录页面且允许所有用户登录 .formLogin() .loginPage("/login") //The updated configuration specifies the location of the log in page 指定自定义登录页面 .permitAll(); // 允许所有用户访问登录页面. The formLogin().permitAll() 方法 .and .logout() //logouts 设置 .logoutUrl("/my/logout") // 指定注销路径 .logoutSuccessUrl("/my/index") //指定成功注销后跳转到指定的页面 .logoutSuccessHandler(logoutSuccessHandler) //指定成功注销后处理类 如果使用了logoutSuccessHandler()的话, logoutSuccessUrl()就会失效 .invalidateHttpSession(true) // httpSession是否有效时间,如果使用了 SecurityContextLogoutHandler,其将被覆盖 .addLogoutHandler(logoutHandler) //在最后增加默认的注销处理类LogoutHandler .deleteCookies(cookieNamesToClear);//指定注销成功后remove cookies //增加在FilterSecurityInterceptor前添加自定义的myFilterSecurityInterceptor http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class); } 复制代码NOTE:此示例只供参考4. filter顺序Spring Security filter顺序:Filter Class说明ChannelProcessingFilter访问协议控制过滤器,可能会将我们重新定向到另外一种协议,从http转换成httpsSecurityContextPersistenceFilter创建SecurityContext安全上下文信息和request结束时清空SecurityContextHolderConcurrentSessionFilter并发访问控制过滤器,主要功能:SessionRegistry中获取SessionInformation来判断session是否过期,从而实现并发访问控制。HeaderWriterFilter给http response添加一些HeaderCsrfFilter跨域过滤器,跨站请求伪造保护FilterLogoutFilter处理退出登录的FilterX509AuthenticationFilter添加X509预授权处理机制支持CasAuthenticationFilter认证filter,经过这些过滤器后SecurityContextHolder中将包含一个完全组装好的Authentication对象,从而使后续鉴权能正常执行UsernamePasswordAuthenticationFilter认证的filter,经过这些过滤器后SecurityContextHolder中将包含一个完全组装好的Authentication对象,从而使后续鉴权能正常执行。表单认证是最常用的一个认证方式。BasicAuthenticationFilter认证filter,经过这些过滤器后SecurityContextHolder中将包含一个完全组装好的Authentication对象,从而使后续鉴权能正常执行SecurityContextHolderAwareRequestFilter此过滤器对ServletRequest进行了一次包装,使得request具有更加丰富的APIJaasApiIntegrationFilter(JAAS)认证方式filterRememberMeAuthenticationFilter记忆认证处理过滤器,即是如果前面认证过滤器没有对当前的请求进行处理,启用了RememberMe功能,会从cookie中解析出用户,并进行认证处理,之后在SecurityContextHolder中存入一个Authentication对象。AnonymousAuthenticationFilter匿名认证处理过滤器,当SecurityContextHolder中认证信息为空,则会创建一个匿名用户存入到SecurityContextHolder中SessionManagementFilter会话管理Filter,持久化用户登录信息,可以保存到session中,也可以保存到cookie或者redis中ExceptionTranslationFilter异常处理过滤器,主要拦截后续过滤器(FilterSecurityInterceptor)操作中抛出的异常。FilterSecurityInterceptor安全拦截过滤器类,获取当前请求url对应的ConfigAttribute,并调用accessDecisionManager进行访问授权决策。spring security的默认filter链:SecurityContextPersistenceFilter ->HeaderWriterFilter ->LogoutFilter ->UsernamePasswordAuthenticationFilter ->RequestCacheAwareFilter ->SecurityContextHolderAwareRequestFilter ->SessionManagementFilter ->ExceptionTranslationFilter ->FilterSecurityInterceptor 复制代码在上节我们已分析了核心的filter源码以及功能。可回看上节源码分析更加深入的了解各个filter工作原理。总结:在认证和访问授权过程前,首先必须进行WebSecurityConfigurer符合自身应用的security Configurer,也要清楚filter链的先后顺序,才能更好理解spring security的工作原理以及在项目中出现的问题定位。了解完准备工作,接下来将展开对认证和访问授权模块的工作流程研究以及项目示例分析。最后如有错误可评论告知。各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
想要深入spring security的authentication (身份验证)和access-control(访问权限控制)工作流程,必须清楚spring security的主要技术点包括关键接口、类以及抽象类如何协同工作进行authentication 和access-control的实现。1.spring security 认证和授权流程常见认证和授权流程可以分成:A user is prompted to log in with a username and password (用户用账密码登录)The system (successfully) verifies that the password is correct for the username(校验密码正确性)The context information for that user is obtained (their list of roles and so on).(获取用户信息context,如权限)A security context is established for the user(为用户创建security context)The user proceeds, potentially to perform some operation which is potentially protected by an access control mechanism which checks the required permissions for the operation against the current security context information.(访问权限控制,是否具有访问权限)1.1 spring security 认证上述前三点为spring security认证验证环节:通常通过AbstractAuthenticationProcessingFilter过滤器将账号密码组装成Authentication实现类UsernamePasswordAuthenticationToken;将token传递给AuthenticationManager验证是否有效,而AuthenticationManager通常使用ProviderManager实现类来检验;AuthenticationManager认证成功后将返回一个拥有详细信息的Authentication object(包括权限信息,身份信息,细节信息,但密码通常会被移除);通过SecurityContextHolder.getContext().getAuthentication().getPrincipal()将Authentication设置到security context中。1.2 spring security访问授权通过FilterSecurityInterceptor过滤器入口进入;FilterSecurityInterceptor通过其继承的抽象类的AbstractSecurityInterceptor.beforeInvocation(Object object)方法进行访问授权,其中涉及了类AuthenticationManager、AccessDecisionManager、SecurityMetadataSource等。根据上述描述的过程,我们接下来主要去分析其中涉及的一下Component、Service、Filter。2.核心组件(Core Component )2.1 SecurityContextHolderSecurityContextHolder提供对SecurityContext的访问,存储security context(用户信息、角色权限等),而且其具有下列储存策略即工作模式:SecurityContextHolder.MODE_THREADLOCAL(默认):使用ThreadLocal,信息可供此线程下的所有的方法使用,一种与线程绑定的策略,此天然很适合Servlet Web应用。SecurityContextHolder.MODE_GLOBAL:使用于独立应用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL:具有相同安全标示的线程修改SecurityContextHolder的工作模式有两种方法 :设置一个系统属性(system.properties) : spring.security.strategy;调用SecurityContextHolder静态方法setStrategyName()在默认ThreadLocal策略中,SecurityContextHolder为静态方法获取用户信息为:Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal(); if (principal instanceof UserDetails) { String username = ((UserDetails)principal).getUsername(); } else { String username = principal.toString(); } 复制代码但是一般不需要自身去获取。 其中getAuthentication()返回一个Authentication认证主体,接下来分析Authentication、UserDetails细节。2.2 AuthenticationSpring Security使用一个Authentication对象来描述当前用户的相关信息,其包含用户拥有的权限信息列表、用户细节信息(身份信息、认证信息)。Authentication为认证主体在spring security中时最高级别身份/认证的抽象,常见的实现类UsernamePasswordAuthenticationToken。Authentication接口源码:public interface Authentication extends Principal, Serializable { //权限信息列表,默认GrantedAuthority接口的一些实现类 Collection<? extends GrantedAuthority> getAuthorities(); //密码信息 Object getCredentials(); //细节信息,web应用中的实现接口通常为 WebAuthenticationDetails,它记录了访问者的ip地址和sessionId的值 Object getDetails(); //通常返回值为UserDetails实现类 Object getPrincipal(); boolean isAuthenticated(); void setAuthenticated(boolean var1) throws IllegalArgumentException; } 复制代码前面两个组件都涉及了UserDetails,以及GrantedAuthority其到底是什么呢?2.3小节分析。2.3 UserDetails&GrantedAuthorityUserDetails提供从应用程序的DAO或其他安全数据源构建Authentication对象所需的信息,包含GrantedAuthority。其官方实现类为User,开发者可以实现其接口自定义UserDetails实现类。其接口源码:public interface UserDetails extends Serializable { Collection<? extends GrantedAuthority> getAuthorities(); String getPassword(); String getUsername(); boolean isAccountNonExpired(); boolean isAccountNonLocked(); boolean isCredentialsNonExpired(); boolean isEnabled(); } 复制代码UserDetails与Authentication接口功能类似,其实含义即是Authentication为用户提交的认证凭证(账号密码),UserDetails为系统中用户正确认证凭证,在UserDetailsService中的loadUserByUsername方法获取正确的认证凭证。其中在getAuthorities()方法中获取到GrantedAuthority列表是代表用户访问应用程序权限范围,此类权限通常是“role(角色)”,例如ROLE_ADMINISTRATOR或ROLE_HR_SUPERVISOR。GrantedAuthority接口常见的实现类SimpleGrantedAuthority。3. 核心服务类(Core Services)3.1 AuthenticationManager、ProviderManager以及AuthenticationProviderAuthenticationManager是认证相关的核心接口,是认证一切的起点。但常见的认证流程都是AuthenticationManager实现类ProviderManager处理,而且ProviderManager实现类基于委托者模式维护AuthenticationProvider 列表用于不同的认证方式。例如:使用账号密码认证方式DaoAuthenticationProvider实现类(继承了AbstractUserDetailsAuthenticationProvide抽象类),其为默认认证方式,进行数据库库获取认证数据信息。游客身份登录认证方式AnonymousAuthenticationProvider实现类从cookies获取认证方式RememberMeAuthenticationProvider实现类 AuthenticationProvider为ProviderManager源码分析:public Authentication authenticate(Authentication authentication) throws AuthenticationException { Class<? extends Authentication> toTest = authentication.getClass(); AuthenticationException lastException = null; Authentication result = null; //AuthenticationProvider列表依次认证 for (AuthenticationProvider provider : getProviders()) { if (!provider.supports(toTest)) { continue; } try { //每个AuthenticationProvider进行认证 result = provider.authenticate(authentication) if (result != null) { copyDetails(authentication, result); break; } } .... catch (AuthenticationException e) { lastException = e; } } //进行父类AuthenticationProvider进行认证 if (result == null && parent != null) { // Allow the parent to try. try { result = parent.authenticate(authentication); } catch (AuthenticationException e) { lastException = e; } } // 如果有Authentication信息,则直接返回 if (result != null) { if (eraseCredentialsAfterAuthentication && (result instanceof CredentialsContainer)) { //清除密码 ((CredentialsContainer) result).eraseCredentials(); } //发布登录成功事件 eventPublisher.publishAuthenticationSuccess(result); return result; } //如果都没认证成功,抛出异常 if (lastException == null) { lastException = new ProviderNotFoundException(messages.getMessage( "ProviderManager.providerNotFound", new Object[] { toTest.getName() }, "No AuthenticationProvider found for {0}")); } prepareException(lastException, authentication); throw lastException; } 复制代码ProviderManager 中的AuthenticationProvider列表,会依照次序去认证,默认策略下,只需要通过一个AuthenticationProvider的认证,即可被认为是登录成功,而且AuthenticationProvider认证成功后返回一个Authentication实体,并为了安全会进行清除密码。如果所有认证器都无法认证成功,则ProviderManager 会抛出一个ProviderNotFoundException异常。3.2 UserDetailsServiceUserDetailsService接口作用是从特定的地方获取认证的数据源(账号、密码)。如何获取到系统中正确的认证凭证,通过loadUserByUsername(String username)获取认证信息,而且其只有一个方法:UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; 复制代码其常见的实现类从数据获取的JdbcDaoImpl实现类,从内存中获取的InMemoryUserDetailsManager实现类,不过我们可以实现其接口自定义UserDetailsService实现类,如下:public class CustomUserService implements UserDetailsService { @Autowired //用户mapper private UserInfoMapper userInfoMapper; @Autowired //用户权限mapper private PermissionInfoMapper permissionInfoMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserInfoDTO userInfo = userInfoMapper.getUserInfoByUserName(username); if (userInfo != null) { List<PermissionInfoDTO> permissionInfoDTOS = permissionInfoMapper.findByAdminUserId(userInfo.getId()); List<GrantedAuthority> grantedAuthorityList = new ArrayList<>(); //组装权限GrantedAuthority object for (PermissionInfoDTO permissionInfoDTO : permissionInfoDTOS) { if (permissionInfoDTO != null && permissionInfoDTO.getPermissionName() != null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority( permissionInfoDTO.getPermissionName()); grantedAuthorityList.add(grantedAuthority); } } //返回用户信息 return new User(userInfo.getUserName(), userInfo.getPasswaord(), grantedAuthorityList); }else { //抛出用户不存在异常 throw new UsernameNotFoundException("admin" + username + "do not exist"); } } } 复制代码 3.3 AccessDecisionManager&SecurityMetadataSourceAccessDecisionManager是由AbstractSecurityInterceptor调用,负责做出最终的访问控制决策。AccessDecisionManager接口源码://访问控制决策 void decide(Authentication authentication, Object secureObject,Collection<ConfigAttribute> attrs) throws AccessDeniedException; //是否支持处理传递的ConfigAttribute boolean supports(ConfigAttribute attribute); //确认class是否为AccessDecisionManager boolean supports(Class clazz); 复制代码SecurityMetadataSource包含着AbstractSecurityInterceptor访问授权所需的元数据(动态url、动态授权所需的数据),在AbstractSecurityInterceptor授权模块中结合AccessDecisionManager进行访问授权。其涉及了ConfigAttribute。SecurityMetadataSource接口:Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException; Collection<ConfigAttribute> getAllConfigAttributes(); boolean supports(Class<?> clazz); 复制代码我们还可以自定义SecurityMetadataSource数据源,实现接口FilterInvocationSecurityMetadataSource。例:public class MyFilterSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { public List<ConfigAttribute> getAttributes(Object object) { FilterInvocation fi = (FilterInvocation) object; String url = fi.getRequestUrl(); String httpMethod = fi.getRequest().getMethod(); List<ConfigAttribute> attributes = new ArrayList<ConfigAttribute>(); // Lookup your database (or other source) using this information and populate the // list of attributes return attributes; } public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } public boolean supports(Class<?> clazz) { return FilterInvocation.class.isAssignableFrom(clazz); } } 复制代码3.4 PasswordEncoder为了存储安全,一般要对密码进行算法加密,而spring security提供了加密PasswordEncoder接口。其实现类有使用BCrypt hash算法实现的BCryptPasswordEncoder,SCrypt hashing 算法实现的SCryptPasswordEncoder实现类,实现类内部实现可看源码分析。而PasswordEncoder接口只有两个方法:public interface PasswordEncoder { //密码加密 String encode(CharSequence rawPassword); //密码配对 boolean matches(CharSequence rawPassword, String encodedPassword); } 复制代码4 核心 Security 过滤器(Core Security Filters)4.1 FilterSecurityInterceptorFilterSecurityInterceptor是Spring security授权模块入口,该类根据访问的用户的角色,权限授权访问那些资源(访问特定路径应该具备的权限)。FilterSecurityInterceptor封装FilterInvocation对象进行操作,所有的请求到了这一个filter,如果这个filter之前没有执行过的话,那么首先执行其父类AbstractSecurityInterceptor提供的InterceptorStatusToken token = super.beforeInvocation(fi),在此方法中使用AuthenticationManager获取Authentication中用户详情,使用ConfigAttribute封装已定义好访问权限详情,并使用AccessDecisionManager.decide()方法进行访问权限控制。FilterSecurityInterceptor源码分析:public void invoke(FilterInvocation fi) throws IOException, ServletException { if ((fi.getRequest() != null) && (fi.getRequest().getAttribute(FILTER_APPLIED) != null) && observeOncePerRequest) { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } else { // first time this request being called, so perform security checking if (fi.getRequest() != null && observeOncePerRequest) { fi.getRequest().setAttribute(FILTER_APPLIED, Boolean.TRUE); } //回调其继承的抽象类AbstractSecurityInterceptor的方法 InterceptorStatusToken token = super.beforeInvocation(fi); try { fi.getChain().doFilter(fi.getRequest(), fi.getResponse()); } finally { super.finallyInvocation(token); } super.afterInvocation(token, null); } } 复制代码AbstractSecurityInterceptor源码分析:protected InterceptorStatusToken beforeInvocation(Object object) { .... //获取所有访问权限(url-role)属性列表(已定义在数据库或者其他地方) Collection<ConfigAttribute> attributes = this.obtainSecurityMetadataSource() .getAttributes(object); .... //获取该用户访问信息(包括url,访问权限) Authentication authenticated = authenticateIfRequired(); // Attempt authorization try { //进行授权访问 this.accessDecisionManager.decide(authenticated, object, attributes); }catch .... } 复制代码4.2 UsernamePasswordAuthenticationFilterUsernamePasswordAuthenticationFilter使用username和password表单登录使用的过滤器,也是最为常用的过滤器。其源码:public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //获取表单中的用户名和密码 String username = obtainUsername(request); String password = obtainPassword(request); ... username = username.trim(); //组装成username+password形式的token UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken( username, password); // Allow subclasses to set the "details" property setDetails(request, authRequest); //交给内部的AuthenticationManager去认证,并返回认证信息 return this.getAuthenticationManager().authenticate(authRequest); } 复制代码其主要代码为创建UsernamePasswordAuthenticationToken的Authentication实体以及调用AuthenticationManager进行authenticate认证,根据认证结果执行successfulAuthentication或者unsuccessfulAuthentication,无论成功失败,一般的实现都是转发或者重定向等处理,不再细究AuthenticationSuccessHandler和AuthenticationFailureHandle。兴趣的可以研究一下其父类AbstractAuthenticationProcessingFilter过滤器。4.3 AnonymousAuthenticationFilterAnonymousAuthenticationFilter是匿名登录过滤器,它位于常用的身份认证过滤器(如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、RememberMeAuthenticationFilter)之后,意味着只有在上述身份过滤器执行完毕后,SecurityContext依旧没有用户信息,AnonymousAuthenticationFilter该过滤器才会有意义——基于用户一个匿名身份。 AnonymousAuthenticationFilter源码分析:public class AnonymousAuthenticationFilter extends GenericFilterBean implements InitializingBean { ... public AnonymousAuthenticationFilter(String key) { this(key, "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS")); } ... public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { if (SecurityContextHolder.getContext().getAuthentication() == null) { //创建匿名登录Authentication的信息 SecurityContextHolder.getContext().setAuthentication( createAuthentication((HttpServletRequest) req)); ... } chain.doFilter(req, res); } //创建匿名登录Authentication的信息方法 protected Authentication createAuthentication(HttpServletRequest request) { AnonymousAuthenticationToken auth = new AnonymousAuthenticationToken(key, principal, authorities); auth.setDetails(authenticationDetailsSource.buildDetails(request)); return auth; } } 复制代码4.4 SecurityContextPersistenceFilterSecurityContextPersistenceFilter的两个主要作用便是request来临时,创建SecurityContext安全上下文信息和request结束时清空SecurityContextHolder。源码后续分析。小节总结:. AbstractAuthenticationProcessingFilter:主要处理登录. FilterSecurityInterceptor:主要处理鉴权总结 经过上面对核心的Component、Service、Filter分析,初步了解了Spring Security工作原理以及认证和授权工作流程。Spring Security认证和授权还有很多负责的过程需要深入了解,所以下次会对认证模块和授权模块进行更具体工作流程分析以及案例呈现。最后以上纯粹个人结合博客和官方文档总结,如有错请指出!各位看官还可以吗?喜欢的话,动动手指点个💗,点个关注呗!!谢谢支持!
2022年05月