首页 文章 精选 留言 我的

精选列表

搜索[实践],共10001篇文章
优秀的个人博客,低调大师

Spring Boot 启动优化实践

作者:vivo 互联网服务器团队- Liu Di 本文系统性分析并优化了一个Spring Boot项目启动耗时高达 280 秒的问题。通过识别瓶颈、优化分库分表加载逻辑、异步初始化耗时任务等手段,最终将启动耗时缩短至 159 秒,提升近 50%。文章涵盖启动流程分析、性能热点识别、异步初始化设计等关键技术细节,适用于大型Spring Boot项目的性能优化参考。 文章太长?1分钟看图抓住核心观点👇 一、前言 随着业务的发展,笔者项目对应的Spring Boot工程的依赖越来越多。随着依赖数量的增长,Spring 容器需要加载更多组件、解析复杂依赖并执行自动装配,导致项目启动时间显著增长。在日常开发或测试过程中,一旦因为配置变更或者其他热部署不生效的变更时,项目重启就需要等待很长的时间影响代码的交付。加快Spring项目的启动可以更好的投入项目中,提升开发效率。 整体环境介绍: Spring版本:4.3.22 Spring Boot版本:1.5.19 CPU:i5-9500 内存:24GB 优化前启动耗时:280秒 二、Spring Boot项目启动流程介绍 Spring Boot项目主要启动流程都在org.spring- framework.boot.SpringApplication#run(java.lang.String...)方法中: public ConfigurableApplicationContext run(String... args) { StopWatch stopWatch = new StopWatch(); stopWatch.start(); // Spring上下文 ConfigurableApplicationContext context = null; FailureAnalyzers analyzers = null; configureHeadlessProperty(); // 初始化SpringApplicationRunListener监听器 SpringApplicationRunListeners listeners = getRunListeners(args); listeners.starting(); try { ApplicationArguments applicationArguments = new DefaultApplicationArguments( args); // 环境准备 ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments); // 打印banner Banner printedBanner = printBanner(environment); // 创建上下文 context = createApplicationContext(); analyzers = new FailureAnalyzers(context); // 容器初始化 prepareContext(context, environment, listeners, applicationArguments, printedBanner); // 刷新容器内容 refreshContext(context); afterRefresh(context, applicationArguments); // 结束监听广播 listeners.finished(context, null); stopWatch.stop(); if (this.logStartupInfo) { new StartupInfoLogger(this.mainApplicationClass) .logStarted(getApplicationLog(), stopWatch); } return context; } catch (Throwable ex) { handleRunFailure(context, listeners, analyzers, ex); throw new IllegalStateException(ex); } } 可以看到在启动流程中,监听器应用在了应用的多个生命周期中。并且Spring Boot中也预留了针对listener的扩展点。我们可以借此实现一个自己的扩展点去监听Spring Boot的每个阶段的启动耗时,实现如下: @Slf4j public class MySpringApplicationRunListener implements SpringApplicationRunListener{ private Long startTime; public MySpringApplicationRunListener(SpringApplication application, String[] args){ } @Override public void starting(){ startTime = System.currentTimeMillis(); log.info("MySpringListener启动开始 {}", LocalTime.now()); } @Override public void environmentPrepared(ConfigurableEnvironment environment){ log.info("MySpringListener环境准备 准备耗时:{}毫秒", (System.currentTimeMillis() - startTime)); startTime = System.currentTimeMillis(); } @Override public void contextPrepared(ConfigurableApplicationContext context){ log.info("MySpringListener上下文准备 耗时:{}毫秒", (System.currentTimeMillis() - startTime)); startTime = System.currentTimeMillis(); } @Override public void contextLoaded(ConfigurableApplicationContext context){ log.info("MySpringListener上下文载入 耗时:{}毫秒", (System.currentTimeMillis() - startTime)); startTime = System.currentTimeMillis(); } @Override public void finished(ConfigurableApplicationContext context, Throwable exception){ log.info("MySpringListener结束 耗时:{}毫秒", (System.currentTimeMillis() - startTime)); startTime = System.currentTimeMillis(); } } 接着还需要在classpath/META-INF目录下新建spring.factories文件,并添加如下文件内容: org.springframework.boot.SpringApplicationRunListener=com.vivo.internet.gameactivity.api.web.MySpringApplicationRunListener 至此,借助Listener机制,我们能够追踪Spring Boot启动各阶段的耗时分布,为后续性能优化提供数据支撑。 contextLoaded事件是在run方法中的prepareContext()结束时调用的,因此contextLoaded事件和finished事件之间仅存在两个语句:refreshContext(context)和afterRefresh (context,applicationArguements)消耗了285秒的时间,调试一下就能发现主要耗时在refreshContext()中。 三、AbstractApplicationContext#refresh refreshContext()最终调用到org.spring-framework.context.support.AbstractApplicationContext#refresh方法中,这个方法主要是beanFactory的预准备、对beanFactory完成创建并进行后置处理、向容器添加bean并且给bean添加属性、实例化所有bean。通过调试发现,finishBeanFactoryInitialization(beanFactory) 方法耗时最久。该方法负责实例化容器中所有的单例 Bean,是启动性能的关键影响点。 四、找出实例化耗时的Bean Spring Boot也是利用的Spring的加载流程。在Spring中可以实现InstantiationAwareBeanPost- Processor接口去在Bean的实例化和初始化的过程中加入扩展点。因此我们可以实现该接口并添加自己的扩展点找到处理耗时的Bean。 @Service public class TimeCostCalBeanPostProcessor implements InstantiationAwareBeanPostProcessor { private Map<String, Long> costMap = Maps.newConcurrentMap(); @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException { if (!costMap.containsKey(beanName)) { costMap.put(beanName, System.currentTimeMillis()); } return null; } @Override public boolean postProcessAfterInstantiation(Object bean, String beanName) throws BeansException { return true; } @Override public PropertyValues postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeansException { return pvs; } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (costMap.containsKey(beanName)) { Long start = costMap.get(beanName); long cost = System.currentTimeMillis() - start; // 只打印耗时长的bean if (cost > 5000) { System.out.println("bean: " + beanName + "\ttime: " + cost + "ms"); } } return bean; } } 具体原理就是在Bean开始实例化之前记录时间,在Bean初始化完成后记录结束时间,打印实例化到初始化的时间差获得Bean的加载总体耗时。结果如图: 可以看到有许多耗时在10秒以上的类,接下来可以针对性的做优化。值得注意的是,统计方式为单点耗时计算,未考虑依赖链上下文对整体加载顺序的影响,实际优化还需结合依赖关系分析。 五、singletonDataSource @Bean(name = "singletonDataSource") public DataSource singletonDataSource(DefaultDataSourceWrapper dataSourceWrapper) throws SQLException { //先初始化连接 dataSourceWrapper.getMaster().init(); //构建分库分表数据源 String dataSource0 = "ds0"; Map<String, DataSource> dataSourceMap = new HashMap<>(); dataSourceMap.put(dataSource0, dataSourceWrapper.getMaster()); //分库分表数据源 DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource (dataSourceMap,shardingRuleConfiguration, prop); return shardingDataSource; } singletonDataSource是一个分库分表的数据源,连接池采用的是Druid,分库分表组件采用的是公司内部优化后的中间件。通过简单调试代码发现,整个Bean耗时的过程发生在createDataSource方法,该方法中会调用createMetaData方法去获取数据表的元数据,最终运行到loadDefaultTables方法。该方法如下图,会遍历数据库中所有的表。因此数据库中表越多,整体就越耗时。 笔者的测试环境数据库中有很多的分表,这些分表为了和线上保持一致,分表的数量都和线上是一样的。 因此在测试环境启动时,为了加载这些分表会更加的耗时。可通过将分表数量配置化,使测试环境在不影响功能验证的前提下减少分表数量,从而加快启动速度。 六、初始化异步 activityServiceImpl启动中,主要会进行活动信息的查询初始化,这是一个耗时的操作。类似同样的操作在工程的其他类中也存在。 @Service public class ActivityServiceImpl implements ActivityService, InitializingBean{ // 省略无关代码 @Override public void afterPropertiesSet() throws Exception { initActivity(); } // 省略无关代码 } 可以通过将afterPropertiesSet()异步化的方式加速项目的启动。 观察Spring源码可以注意到afterPropertiesSet方法是在AbstractAutowireCapableBeanFactory# invokeInitMethods中调用的。在这个方法中,不光处理了afterPropertiesSet方法,也处理了init-method。 因此我们可以写一个自己的BeanFactory继承AbstractAutowireCapableBeanFactory,将invokeInitMethods方法进行异步化重写。考虑到AbstractAutowireCapableBeanFactory是个抽象类,有额外的抽象方法需要实现,因此继承该抽象类的子类DefaultListableBeanFactory。具体实现代码如下: public class AsyncInitListableBeanFactory extends DefaultListableBeanFactory{ public AsyncInitBeanFactory(ConfigurableListableBeanFactory beanFactory){ super(beanFactory); } @Override protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd)throws Throwable { if (beanName.equals("activityServiceImpl")) { AsyncTaskExecutor.submitTask(() -> { try { super.invokeInitMethods(beanName, bean, mbd); } catch (Throwable throwable) { throwable.printStackTrace(); } }); } else { super.invokeInitMethods(beanName, bean, mbd); } } } 又因为Spring在refreshContext()方法之前的prepareContext()发放中针对initialize方法提供了接口扩展(applyInitializers())。因此我们可以通过实现该接口并将我们的新的BeanFactory通过反射的方式更新到Spring的初始化流程之前。 public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> { /** * Initialize the given application context. * @param applicationContext the application to configure */ void initialize(C applicationContext); } 改造后的代码如下,新增AsyncAccelerate- Initializer类实现ApplicationContextInitializer接口: public class AsyncBeanFactoryInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> { @SneakyThrows @Override public void initialize(ConfigurableApplicationContext applicationContext){ if (applicationContext instanceof GenericApplicationContext) { AsyncInitListableBeanFactory beanFactory = new AsyncInitListableBeanFactory(applicationContext.getBeanFactory()); Field field = GenericApplicationContext.class.getDeclaredField("beanFactory"); field.setAccessible(true); field.set(applicationContext, beanFactory); } } } public class AsyncBeanInitExecutor{ private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors(); private static final AtomicReference<ThreadPoolExecutor> THREAD_POOL_REF = new AtomicReference<>(); private static final List<Future<?>> FUTURES = new ArrayList<>(); /** * 创建线程池实例 */ private static ThreadPoolExecutor createThreadPoolExecutor(){ int poolSize = CPU_COUNT + 1; return new ThreadPoolExecutor(poolSize, poolSize, 50L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy() ); } /** * 确保线程池已初始化(线程安全) */ private static void ensureThreadPoolExists(){ if (THREAD_POOL_REF.get() != null) { return; } ThreadPoolExecutor executor = createThreadPoolExecutor(); if (!THREAD_POOL_REF.compareAndSet(null, executor)) { executor.shutdown(); // 另一线程已初始化成功 } } /** * 提交异步初始化任务 * * @param task 初始化任务 * @return 提交后的 Future 对象 */ public static Future<?> submitInitTask(Runnable task) { ensureThreadPoolExists(); Future<?> future = THREAD_POOL_REF.get().submit(task); FUTURES.add(future); return future; } /** * 等待所有初始化任务完成并释放资源 */ public static void waitForInitTasks(){ try { for (Future<?> future : FUTURES) { future.get(); } } catch (Exception ex) { throw new RuntimeException("Async init task failed", ex); } finally { FUTURES.clear(); shutdownThreadPool(); } } /** * 关闭线程池并重置引用 */ private static void shutdownThreadPool(){ ThreadPoolExecutor executor = THREAD_POOL_REF.getAndSet(null); if (executor != null) { executor.shutdown(); } } } 实现类后,还需要在META-INF/spring.factories下新增说明org.springframework.context. ApplicationContextInitializer=com.xxx.AsyncAccelerateInitializer,这样这个类才能真正生效。 这样异步化以后还有一个点需要注意,如果该初始化方法执行耗时很长,那么会存在Spring容器已经启动完成,但是异步初始化任务没执行完的情况,可能会导致空指针等异常。为了避免这种问题的发生,还要借助于Spring容器启动中finishRefresh()方法,监听对应事件,确保异步任务执行完成之后,再启动容器。 public class AsyncInitCompletionListener implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, PriorityOrdered{ private ApplicationContext currentContext; @Override public void setApplicationContext(ApplicationContext applicationContext)throws BeansException { this.currentContext = applicationContext; } @Override public void onApplicationEvent(ContextRefreshedEvent event){ if (event.getApplicationContext() == currentContext) { AsyncBeanInitExecutor.waitForInitTasks(); } } @Override public int getOrder(){ return Ordered.HIGHEST_PRECEDENCE; } } 七、总结 启动优化后的项目实际测试结果如下: 通过异步化初始化和分库分表加载优化,项目启动时间从 280 秒缩短至 159 秒,提升约 50%。这对于提升日常开发效率、加快测试与联调流程具有重要意义。

优秀的个人博客,低调大师

PikiwiDB(Pika) 3.5 最佳实践

PikiwiDB(Pika) 是 360 技术中台中间件团队基于 RocksDB 开发的大容量类 Redis 存储系统,力求在完全兼容 Redis 协议、继承 Redis 便捷运维设计的前提下通过持久化存储方式解决 Redis 在大容量场景下主从同步代价高、恢复时间慢、单线程相对脆弱、内存成本高等问题。 我们根据 360 内部的 PikiwiDB(Pika) 使用经验及社区用户的问题反馈,整理了本文并在这里分享给大家。 之一 在微信群(群管理员微信号:PikiwiDB)中提问时,请主动带上版本号,可大幅度加快问题解决速度。 之二 PikiwiDB(Pika) 已在 2024 年 5 月更新至 3.5.4,但仍然有大量用户停留在 3.3.6 或3.3.2,我们建议使用 3.5.4 的最新版(预计本周内发布 v4.0.0),你会发现你遇到的很多问题都在我们的 bug 修复列表中。 之三 PikiwiDB(Pika) 的线程数量 thread-num 建议设置为 CPU core 数目的 80% 左右,如果是单机多实例的部署,每个 PikiwiDB(Pika) 实例的线程数量可以酌情降低,但不建议低于 CPU core 数的 1/2。 之四 PikiwiDB(Pika) 的工作线程池数量 thread-pool-size 建议和 CPU core 数目一致,如果是单机多实例的部署,每个 PikiwiDB(Pika) 实例的线程数量可以酌情降低,但不建议低于 1/2 CPU core 数。 之五 PikiwiDB(Pika) 的性能和 IO 性能息息相关,如果对耗时非常敏感,建议使用 NVMe SSD。另外,主从服务器的硬件规格应当尽量一致。 之六 在使用 PikiwiDB(Pika) 复合数据结构(hash,list,zset,zset)时,尽量确保每个 key 中的二级 key(或者成为 field)不要太多(不要超过 1 万个),在业务层或者代理层对大 key 符合数据结构进行拆分(类似于分库分表), 这样可以避免超大 key 带来很多潜在的性能风险。 之七 root-connection-num 参数非常有用,意为“允许通过 127.0.0.1 登录 PikiwiDB(Pika) 的连接数”,它不会被算进客户端最大连接数配置项 maxclients,因此在发生异常 maxclients 被用尽的场景中,管理员仍然可以登录 PikiwiDB(Pika) 所在服务器并通过 127.0.0.1 登入 PikiwiDB(Pika) 处理问题,可以认为是超级管理员通道。 之八 client kill 命令被加强了,如果你想一次性杀掉当前 PikiwiDB(Pika) 的所有客户端连接,只需要执行 client kill all 命令即可。注意,主从同步的网络连接不受影响。 之九 适当地调整 timeout 参数,PikiwiDB(Pika) 会主动断开不活跃时间超过 timeout 值的连接,避免连接数耗尽。由于网络连接会占用主机内存,因此合理的配置 timeout 参数也能够在一定程度上降低 PikiwiDB(Pika) 的内存使用量。 之十 PikiwiDB(Pika) 的内存占用主要集中在 SST 文件的 cache 和网络连接内存占用量,通常网络连接内存量会比 SST 的 cache 大,PikiwiDB(Pika) 目前已支持连接申请内存的动态调整与回收,因此连接占用的总内存大小是可以粗略估算的,如果你的 PikiwiDB(Pika) 内存占用远超预估(如大于 10GiB),那么可能为你当前使用的版本存在内存泄漏问题,尝试依次执行命令 client kill all 对连接内存进行强制回收,或者升级到最新版本。 之十一 非常不建议单副本运行 PikiwiDB(Pika),单副本的数据安全性无法保障,诸如 RocksDB Bug 或者资源不够(如:ERR IO error: While fdatasync: /data1/db/zsets/16566747.log: Cannot allocate memory)导致 RocksDB 存储数据被污染,此时无法全量恢复数据。 最简集群状态应为一主一从。 之十二 如果 PikiwiDB(Pika) 单副本运行(非主从集群),只在乎性能,且不在乎数据安全性(如缓存场景),可以考虑通过关闭 binlog(将 write-binlog 参数设置为 no)来提高写入性能。 之十三 PikiwiDB(Pika) v3.5.2 以及之后的版本提供了关闭 RocksDB WAL (DisableWAL true) 的命令,如果你的 PikiwiDB(Pika) 实例出现间断性的写性能阻塞的情况,你可以通过关闭 WAL 命令暂时关闭 WAL,这种方式有断电情况下数据丢失的风险,待性能恢复时,请及时再打开。对数据完整性要求不高时,建议关闭 WAL。 之十四 PikiwiDB(Pika) 的数据目录中有大量的 SST 文件,这些文件随着 PikiwiDB(Pika) 数据量的增加而增加,建议为 PikiwiDB(Pika) 配置一个较大的 open_file_limit ,以避免 fd 不够用,如果不希望 Pika 占用太多的文件描述符,可以通过适当增大单个 SST 的体积来降低 SST 的总数量,对应参数为 target-file-size-base。 之十五 不要修改 log 目录中的 write2file 文件和 manifest。write2file 记录了 binlog 文件列表等关键信息,而 manifest 则记录了 RocksDB 的 version 信息,二者关乎 PikiwiDB(Pika) 实例重启后的 binlog 续写及 slave 断点续传时的数据正确性。 之十六 自 PikiwiDB(Pika) v3.5.0 之后的版本摒弃了用 rsync 进程进行全量同步,PikiwiDB(Pika) 进程内部重新实现了一套新的全量同步机制(通过名称为 rsync 的线程传输)。PikiwiDB(Pika) 提供了 rsync 的总传输限速参数 throttle-bytes-per-second 和并发 rsync 线程数 max-rsync-parallel-num,throttle-bytes-per-second 参数的单位是 MiB,建议在千兆环境中该参数设置不应高于 45,而在万兆环境中不应高于 500,以避免 PikiwiDB(Pika) 在全量同步的时候将所在服务器网卡流量用尽而影响到 PikiwiDB(Pika) 服务客户端。 之十七 在 PikiwiDB(Pika) 中执行 “ key * ” 并不会造成 Pika 阻塞(PikiwiDB(Pika) 是多线程的),但在存在巨量 key 的场景下可能会造成临时占用巨量内存(这些内存用于该连接存放 key *的执行结果,会在 “ key * ”执行完毕后释放),因此使用 “ key * ” 一定要小心谨慎。 之十八 如果发现 PikiwiDB(Pika) 有数据但 info keyspace 的显示均为 0,这是因为 Pika 并没有像 Redis 那样对 key 的数量进行实时统计,PikiwiDB(Pika) 中 key 的统计需要人工触发,执行 info keyspace 1,注意执行 info keyspace 是不会触发统计的,没有带上最后的参数 1 将会仅仅展示上一次的统计结果,key 的统计是需要时间的,执行状态可以通过 info stats 中的 is_scaning_keyspace 进行查看,该项值为 yes 表明统计正在进行,为 no 时表明没有正在进行的统计/上一次统计已结束,在统计执行完毕前 info keyspace 不会更新,info keyspace 的数据是存放在内存里的,重启将清零。 之十九 不要在 PikiwiDB(Pika) 执行全量 compact 的时候触发 key 统计(info keyspace 1)或执行 keys *,否则会造成数据体积暂时膨胀直到 key 统计、keys *执行结束。 之二十 对存在大量过期数据的 PikiwiDB(Pika) 实例,compact-cron 配置项可以在固定时段(一般配置为低峰流量时间段)进行过期数据清理。自 PikiwiDB(Pika) v3.5.0 之后还提供了 auto_compact 配置型,启用后 PikiwiDB(Pika) 会自动周期性执行 compact。 异常的数据体积(大于估算值 10%以上),可以通过执行 compact 命令,在 compact 执行完毕后观察数据体积是否恢复正常。 请求耗时突然异常增大,可以通过执行 compact 命令,在 compact 执行完毕后观察请求耗时是否恢复正常。 之二十一 自 PikiwiDB(Pika) v3.5.0 之后可统计过期 key(可通过 info keyspace 1 来触发统计,通过 info keyspace 查看统计结果),统计结果中的 invaild_keys 的值为“已删除/过期但还未被物理删除的 key 的数量”,PikiwiDB(Pika) 会在后台逐步地对已删除/过期的 key 进行物理清理,由于这是一个后台行为,因此在存在大规模过期 key 的场景下这些 key 可能无法被及时清理,因此建议关注该值,若发现无效 key 数量过多可通过 compact 命令进行全面清理,这样能够将未物理清理的无效数据控制在一个较好的程度从而确保 Pika 的性能稳定,如果 PikiwiDB(Pika) 中存储的数据是规律性过期的,例如每个 key 的过期时间为 7 天,那么建议通过配置 compact-cron 参数来实现每天的定时自动进行全量 compact,compact 会占用一定的 IO 资源,因此如果磁盘 IO 压力过大,建议将其配置为业务低峰期执行,例如深夜。 之二十二 write2file 的角色相当于 binlog,建议 write2file 保留周期/数量不低于 48 小时,足够的 write2file 有利于 大数据集群的从库扩容、从库服务器关机维修、从库迁移 等工作,不会因为主库 write2file 过期而被迫全量重传。 之二十三 PikiwiDB(Pika) 的备份生成为快照式,通过硬链接存放在 dump 目录下,以日期为后缀,每天只生成一份,多次生成备份时新的备份会覆盖之前的旧文件。在生成备份快照的时,为了确保数据的一致性 PikiwiDB(Pika) 会暂时阻塞写入,阻塞时间与实际数据量相关,根据测试PikiwiDB(Pika) 生成 500GiB 备份快照仅需 50ms。在写入阻塞的过程中连接不会中断,但 client 会感觉到 “在那一瞬间请求耗时增加了一些”。由于PikiwiDB(Pika)Pika 的快照是 db 目录中 sst 文件的硬连接,因此最初这个目录是不会占用磁盘空间的。 但在 PikiwiDB(Pika) db 目录中的 SST 文件发生了合并、删除后,硬链接的旧文件并不删除,这会导致 PikiwiDB(Pika) 占用的磁盘空间超出预估,所以请根据实际的磁盘空间调整备份保留天数,避免备份太多而造成磁盘空间用尽。 之二十四 如果写入量巨大且磁盘性能不足以满足 RocksDB memtable 的及时刷盘需求,那么 RocksDB 很可能会进入写保护模式(write stall,写入将被全部阻塞),建议更换性能更好的存储系统来支撑,或者降低写入频率(例如将集中写数据的 2 小时拉长到 4 小时),也可适当加大 write-buffer-size 的值来提高 memtable 的总容量从而降低整个 memtable 被写满的可能。 之二十五 PikiwiDB(Pika) 对数据进行了压缩,默认压缩算法为 snappy,并允许改为 zlib,因此每一次数据的存入、读出都需要经过压缩、解压,这对 CPU 有一定的消耗,建议像使用 Redis 一样使用 PikiwiDB(Pika):在 PikiwiDB(Pika) 中关闭压缩,而在 client 中完成数据的压缩、解压,这样不仅能够降低数据体积,还能有效降低 Pikiw。注意关闭和开启压缩后,需要重启 PikiwiDB(Pika) 实例。 之二十六 读写分离很重要,PikiwiDB(Pika) 在常见的主从集群中由于写入是单点的(仅 master 支持写),因此写入性能是有极限的。可通过多个 slave 来共同支撑读流量,因此 PikiwiDB(Pika) 集群的读性能是随着 slave 数量的增加而增加的,所以对于读量很大的场景,建议在业务层代码加入读写分离策略,同时在 PikiwiDB(Pika) 层增加 slave 数量。 之二十七 全量 compact 的原理是逐步对 RocksDB 的每一层做数据合并、清理工作,在这个过程中会新增、删除大量的 SST 文件,因此在执行全量 compact 的时候可以发现数据体积先增大后减小并最终减小到一个稳定值(无效、重复数据合并、清理完毕仅剩有效数据),建议在执行 compact 前确保磁盘空余空间不低于 30%,以避免新增 SST 文件时将磁盘空间耗尽,另外 PikiwiDB(Pika) 支持对指定数据结构进行 compact,例如一个实例中已知 hashtable 结构的无效数据很少但 hashtable 结构数据量很大,set 结构数据量很大且无效数据很多,在这个例子中 hashtable 结构的 compaction(命令是 compact hash) 是没有必要的,你可以通过 compact set 实现只对 set 结构进行 compaction。 注意:在 PikiwiDB v4.0.0 版本之后,不再支持对特定类型的 compaction。因为 PikiwiDB v3.x 使用的存储引擎是 Blackwidow,每个数据类型使用一个 RocksDB,而 v4.0.0 的存储引擎升级为 Floyd,可以在单个 RocksDB 中存储所有类型的数据。 之二十八 PikiwiDB(Pika) 3.5.0 以后的版本支持通过 rate-limiter-bandwidth 配置项以限制磁盘 IO 速率,可以通过调整该配置参数来调整读写速度。在 v4.0.0 之前只支持写限速,在 v4.0.0 之后支持读写限速,可以通过调整配置参数中的 rate-limiter-mode 来设置限速模式。 之二十九 PikiwiDB(Pika) 和 Redis 一样支持慢日志功能,可通过 slowlog 命令查看。slowlog 的原始内容只存于内存中,内存空间有上限,且这个上限可配置,当然如果配置过大会造成 slowlog 占用太多内存。PikiwiDB(Pika) 也允许将 slowlog-write-errorlog 设置为 yes,以把慢日志记录到 pika.ERROR 日志中,用于追溯、分析。 之三十 PikiwiDB(Pika) v3.5.2 以后的版本支持冷热数据分离,并在 Pika 磁盘存储之上增加了内存缓存层(称之为 RedisCache),将用户访问的热数据放在缓存层,冷数据放在磁盘,可减少查询磁盘的次数,提升服务的读性能,不论 PikiwiDB(Pika) 使用的是主从复制模式还是集群模式,可以配置 cache-mode 为 1 ,并设置缓存的大小和个数,以提升读性能。如果实例内存较小,不足以支撑缓存层的资源耗费,你可以选择将 cache-mode 设置成为 0 将缓存层关闭掉。 之三十一 PikiwiDB(Pika) 3.5.3 以后的版本支持了 Redis ACL 功能,设置用户密码的方式发生了变化,ACL的认证方式和 Redis 保持一致,在 config 文件中按照 ACL 规则对 user 进行配置。PikiwiDB(Pika) 3.5.3 仍然兼容以前旧版本的认证方式。 之三十二 PikiwiDB(Pika) 3.5.3 以后的版本支持快、慢命令分离,有快、慢两个线程池,可以防止慢命令对快命令线程池阻塞的影响。可以通过 slow-cmd-list 配置项设置慢命令列表,通过设置 slow-cmd-thread-pool-size 设置慢命令线程池个数。 之三十三 欲知后事如何,且待微信群里分解。请添加 PikiwiDB 小助手【微信号: PikiwiDB】为好友,它会拉您加入官方微信群。

优秀的个人博客,低调大师

GaussDB表设计最佳实践

目录 如何选择存储模型 如何选择数据分布方式 如何选择分布列 其他最佳设计建议 使用局部聚簇 使用分区表 选择数据类型 如何选择存储模型 进行数据库设计时,表设计上的一些关键项将严重影响后续整库的查询性能。表设计对数据存储也有影响:好的表设计能够减少I/O操作及最小化内存使用,进而提升查询性能。 表的存储模型选择是表定义的第一步。客户业务属性是表的存储模型的决定性因素,依据下面表格选择适合当前业务的存储模型。 存储模型 适用场景 行存 点查询(返回记录少,基于索引的简单查询)。增删改比较多的场景。 列存 统计分析类查询 (group , join多的场景)。 如何选择数据分布方式 复制表(Replication)方式将表中的全量数据在集群的每一个DN实例上保留一份。主要适用于记录集较小的表。这种存储方式的优点是每个DN上都有该表的全量数据,在join操作中可以避免数据重分布操作,从而减小网络开销,同时减少了plan segment(每个plan segment都会起对应的线程);缺点是每个DN都保留了表的完整数据,造成数据的冗余。一般情况下只有较小的维度表才会定义为Replication表。 哈希(Hash)表将表中某一个或几个字段进行hash运算后,生成对应的hash值,根据DN实例与哈希值的映射关系获得该元组的目标存储位置。对于Hash分布表,在读/写数据时可以利用各个节点的IO资源,大大提升表的读/写速度。一般情况下大表定义为Hash表。 范围(Range)和列表(List)分布是由用户自定义的分布策略,根据分布列的取值落入满足一定范围或者具体值的对应目标DN,这两种分布方式便于用户灵活地进行数据管理,但对用户本身的数据抽象能力有一定的要求。 策略 描述 适用场景 Hash 表数据通过hash方式散列到集群中的所有DN实例上。 数据量较大的事实表。 Replication 集群中每一个DN实例上都有一份全量表数据。 小表、维度表。 Range 表数据对指定列按照范围进行映射,分布到对应DN。 用户需要自定义分布规则的场景。 List 表数据对指定列按照具体值进行映射,分布到对应DN。 用户需要自定义分布规则的场景。 如图1所示,复制表如图中的表T1,哈希表如图中的表T2。 图1复制表和哈希表 如何选择分布列 Hash分布表的分布列选取至关重要,需要满足以下原则: 列值应比较离散,以便数据能够均匀分布到各个DN。例如,考虑选择表的主键为分布列,如在人员信息表中选择身份证号码为分布列。 在满足上述条件的情况下,考虑选择查询中的连接条件为分布列,以便Join任务能够下推到DN中执行,且减少DN之间的通信数据量。 对于Hash分表策略,如果分布列选择不当,可能导致数据倾斜,查询时出现部分DN的I/O短板,从而影响整体查询性能。因此在采用Hash分表策略之后需对表的数据进行数据倾斜性检查,以确保数据在各个DN上是均匀分布的。可以使用以下SQL检查数据倾斜性 select xc_node_id, count(1) from tablename group by xc_node_id order by xc_node_id desc; 其中xc_node_id对应DN,一般来说,不同DN的数据量相差5%以上即可视为倾斜,如果相差10%以上就必须要调整分布列。 GaussDB支持多分布列特性,可以更好地满足数据分布的均匀性要求。 Range/List分布表的分布列由用户根据实际需要进行选择。除了需选择合适的分布列,还需要注意分布规则对数据分布的影响。 其他最佳设计建议 使用局部聚簇 局部聚簇(Partial Cluster Key)是列存下的一种技术。这种技术可以通过min/max稀疏索引较快的实现基表扫描的filter过滤。Partial Cluster Key可以指定多列,但是一般不建议超过2列。Partial Cluster Key的选取原则: 受基表中的简单表达式约束。这种约束一般形如col op const,其中col为列名,op为操作符 =、>、>=、<=、<,const为常量值。 尽量采用选择度比较高(过滤掉更多数据)的简单表达式中的列。 尽量把选择度比较高的约束col放在Partial Cluster Key中的前面。 尽量把枚举类型的列放在Partial Cluster Key中的前面。 使用分区表 分区表是把逻辑上的一张表根据某种方案分成几张物理块进行存储。这张逻辑上的表称之为分区表,物理块称之为分区。分区表是一张逻辑表,不存储数据,数据实际是存储在分区上的。分区表和普通表相比具有以下优点: 改善查询性能:对分区对象的查询可以仅搜索自己关心的分区,提高检索效率。 增强可用性:如果分区表的某个分区出现故障,表在其他分区的数据仍然可用。 方便维护:如果分区表的某个分区出现故障,需要修复数据,只修复该分区即可。 GaussDB支持的分区表为范围分区表。 范围分区表:将数据基于范围映射到每一个分区。这个范围是由创建分区表时指定的分区键决定的。分区键经常采用日期,例如将销售数据按照月份进行分区。 选择数据类型 高效数据类型,主要包括以下三方面: 尽量使用执行效率比较高的数据类型 一般来说整型数据运算(包括=、>、<、≧、≦、≠等常规的比较运算,以及group by)的效率比字符串、浮点数要高。比如某客户场景中对列存表进行点查询,filter条件在一个numeric列上,执行时间为10+s;修改numeric为int类型之后,执行时间缩短为1.8s左右。 尽量使用短字段的数据类型 长度较短的数据类型不仅可以减小数据文件的大小,提升IO性能;同时也可以减小相关计算时的内存消耗,提升计算性能。比如对于整型数据,如果可以用smallint就尽量不用int,如果可以用int就尽量不用bigint。 使用一致的数据类型 表关联列尽量使用相同的数据类型。如果表关联列数据类型不同,数据库必须动态地转化为相同的数据类型进行比较,这种转换会带来一定的性能开销。

优秀的个人博客,低调大师

Function源码解析与实践

作者:陈昌浩 1 导读 if…else…在代码中经常使用,听说可以通过 Java 8 的 Function 接口来消灭 if…else…!Function 接口是什么?如果通过 Function 接口接口消灭 if…else…呢?让我们一起来探索一下吧。 2 Function 接口 Function 接口就是一个有且仅有一个抽象方法,但是可以有多个非抽象方法的接口,Function 接口可以被隐式转换为 lambda 表达式。可以通过 FunctionalInterface 注解来校验 Function 接口的正确性。Java 8 允许在接口中加入具体方法。接口中的具体方法有两种,default 方法和 static 方法。 @FunctionalInterfaceinterface TestFunctionService{ void addHttp(String url);} 那么就可以使用 Lambda 表达式来表示该接口的一个实现。 TestFunctionService testFunctionService = url -> System.out.println("http:" + url); 2.1 FunctionalInterface 2.1.1 源码 @Documented@Retention(RetentionPolicy.RUNTIME)@Target(ElementType.TYPE)public @interface FunctionalInterface {} 2.1.2 说明 上图是 FunctionalInterface 的注解说明。通过上面的注解说明,可以知道 FunctionalInterface 是一个注解,用来说明一个接口是函数式接口。 函数式接口只有一个抽象方法。 可以有默认方法,因为默认方法有一个实现,所以不是抽象的。函数接口的实例可以用 lambda 表达式、方法引用或构造函数引用创建。 FunctionalInterface 会校验接口是否满足函数式接口: 类型必须是接口类型,不能是注释类型、枚举或类。 只能有一个抽象方法。 可以有多个默认方法和静态方法。 可以显示覆盖 java.lang.Object 中的抽象方法。 编译器会将满足函数式接口定义的任何接口视为函数式接口,而不管该接口声明中是否使用 FunctionalInterface 注解。 3 Function 接口主要分类 Function 接口主要分类: Function:Function 函数的表现形式为接收一个参数,并返回一个值。 Supplier:Supplier 的表现形式为不接受参数、只返回数据。 Consumer:Consumer 接收一个参数,没有返回值。 Runnable:Runnable 的表现形式为即没有参数也没有返回值。 3.1 Function Function 函数的表现形式为接收一个参数,并返回一个值。 3.1.1 源码 @FunctionalInterfacepublic interface Function<T, R> { R apply(T t); default <V> Function<V, R> compose(Function<? super V, ? extends T> before) { Objects.requireNonNull(before); return (V v) -> apply(before.apply(v)); } default <V> Function<T, V> andThen(Function<? super R, ? extends V> after) { Objects.requireNonNull(after); return (T t) -> after.apply(apply(t)); } static <T> Function<T, T> identity() { return t -> t; }} 3.1.2 方法说明 apply:抽象方法。将此函数应用于给定的参数。参数 t 通过具体的实现返回 R。 compose:default 方法。返回一个复合函数,首先执行 fefore 函数应用于输入,然后将该函数应用于结果。如果任意一个函数的求值引发异常,则将其传递给组合函数的调用者。 andThen:default 方法。返回一个复合函数,该复合函数首先对其应用此函数它的输入,然后对结果应用 after 函数。如果任意一个函数的求值引发异常,则将其传递给组合函数的调用者。 identity:static 方法。返回一个始终返回其输入参数的函数。 3.1.3 方法举例 1)apply 测试代码: public String upString(String str){ Function<String, String> function1 = s -> s.toUpperCase(); return function1.apply(str);} public static void main(String[] args) { System.out.println(upString("hello!")); } 通过 apply 调用具体的实现。执行结果: 2)compose 测试代码: public static void main(String[] args) { Function<String, String> function1 = s -> s.toUpperCase(); Function<String, String> function2 = s -> "my name is "+s; String result = function1.compose(function2).apply("zhangSan"); System.out.println(result);} 执行结果 如结果所示:compose 先执行 function2 后执行 function1。 3)andThen 测试代码: public static void main(String[] args) { Function<String, String> function1 = s -> s.toUpperCase(); Function<String, String> function2 = s -> "my name is "+s; String result = function1.andThen(function2).apply("zhangSan"); System.out.println(result);} 执行结果: 如结果所示: andThen 先执行 function1 后执行 function2。 identity 测试代码: public static void main(String[] args) { Stream<String> stream = Stream.of("order", "good", "lab", "warehouse"); Map<String, Integer> map = stream.collect(Collectors.toMap(Function.identity(), String::length)); System.out.println(map);} 执行结果: 3.2 Supplier Supplier 的表现形式为不接受参数、只返回数据。 3.2.1 源码 @FunctionalInterfacepublic interface Supplier<T> { /** * Gets a result. * * @return a result */ T get();} 3.2.2 方法说明 get:抽象方法。通过实现返回 T。 3.2.3 方法举例 public class SupplierTest { SupplierTest(){ System.out.println(Math.random()); System.out.println(this.toString()); }} public static void main(String[] args) { Supplier<SupplierTest> sup = SupplierTest::new; System.out.println("调用一次"); sup.get(); System.out.println("调用二次"); sup.get();} 执行结果: 如结果所示:Supplier 建立时并没有创建新类,每次调用 get 返回的值不是同一个。 3.3 Consumer Consumer 接收一个参数,没有返回值。 3.3.1 源码 @FunctionalInterfacepublic interface Consumer<T> { void accept(T t); default Consumer<T> andThen(Consumer<? super T> after) { Objects.requireNonNull(after); return (T t) -> { accept(t); after.accept(t); }; }} 3.3.2 方法说明 accept:对给定参数 T 执行一些操作。 andThen:按顺序执行 Consumer -> after ,如果执行操作引发异常,该异常被传递给调用者。 3.3.3 方法举例 public static void main(String[] args) { Consumer<String> consumer = s -> System.out.println("consumer_"+s); Consumer<String> after = s -> System.out.println("after_"+s); consumer.accept("isReady"); System.out.println("========================"); consumer.andThen(after).accept("is coming");} 执行结果: 如结果所示:对同一个参数 T,通过 andThen 方法,先执行 consumer,再执行 fater。 3.4 Runnable Runnable:Runnable 的表现形式为即没有参数也没有返回值。 3.4.1 源码 @FunctionalInterfacepublic interface Runnable { public abstract void run();} 3.4.2 方法说明 run:抽象方法。run 方法实现具体的内容,需要将 Runnale 放入到 Thread 中,通过 Thread 类中的 start()方法启动线程,执行 run 中的内容。 3.4.3 方法举例 public class TestRun implements Runnable { @Override public void run() { System.out.println("TestRun is running!"); }} public static void main(String[] args) { Thread thread = new Thread(new TestRun()); thread.start(); } 执行结果: 如结果所示:当线程实行 start 方法时,执行 Runnable 的 run 方法中的内容。 4 Function 接口用法 Function 的主要用途是可以通过 lambda 表达式实现方法的内容。 4.1 差异处理 原代码: @Datapublic class User { /** * 姓名 */ private String name; /** * 年龄 */ private int age; /** * 组员 */ private List<User> parters;} public static void main(String[] args) { User user =new User(); if(user ==null ||user.getAge() <18 ){ throw new RuntimeException("未成年!"); }} 执行结果: 使用 Function 接口后的代码: @FunctionalInterfacepublic interface testFunctionInfe { /** * 输入异常信息 * @param message */ void showExceptionMessage(String message);} public static testFunctionInfe doException(boolean flag){ return (message -> { if (flag){ throw new RuntimeException(message); } }); } public static void main(String[] args) { User user =new User(); doException(user ==null ||user.getAge() <18).showExceptionMessage("未成年!");} 执行结果: 使用 function 接口前后都抛出了指定的异常信息。 4.2 处理 if…else… 原代码: public static void main(String[] args) { User user =new User(); if(user==null){ System.out.println("新增用户"); }else { System.out.println("更新用户"); }} 使用 Function 接口后的代码: public static void main(String[] args) { User user =new User(); Consumer trueConsumer = o -> { System.out.println("新增用户"); }; Consumer falseConsumer= o -> { System.out.println("更新用户"); }; trueOrFalseMethdo(user).showExceptionMessage(trueConsumer,falseConsumer);}public static testFunctionInfe trueOrFalseMethdo(User user){ return ((trueConsumer, falseConsumer) -> { if(user==null){ trueConsumer.accept(user); }else { falseConsumer.accept(user); } });}@FunctionalInterfacepublic interface testFunctionInfe { /** * 不同分处理不同的事情 * @param trueConsumer * @param falseConsumer */ void showExceptionMessage(Consumer trueConsumer,Consumer falseConsumer);} 执行结果: 4.3 处理多个 if 原代码: public static void main(String[] args) { String flag=""; if("A".equals(flag)){ System.out.println("我是A"); }else if ("B".equals(flag)) { System.out.println("我是B"); }else if ("C".equals(flag)) { System.out.println("我是C"); }else { System.out.println("没有对应的指令"); }} 使用 Function 接口后的代码: public static void main(String[] args) { String flag="B"; Map<String, Runnable> map =initFunctionMap(); trueOrFalseMethdo(map.get(flag)==null).showExceptionMessage(()->{ System.out.println("没有相应指令"); },map.get(flag));}public static Map<String, Runnable> initFunctionMap(){ Map<String,Runnable> result = Maps.newHashMap(); result.put("A",()->{System.out.println("我是A");}); result.put("B",()->{System.out.println("我是B");}); result.put("C",()->{System.out.println("我是C");}); return result;}public static testFunctionInfe trueOrFalseMethdo(boolean flag){ return ((runnable, falseConsumer) -> { if(flag){ runnable.run(); }else { falseConsumer.run(); } });} 执行结果: 5 总结 Function 函数式接口是 java 8 新加入的特性,可以和 lambda 表达式完美结合,是非常重要的特性,可以极大的简化代码。

优秀的个人博客,低调大师

StoneDB读写分离实践方案

在 StoneDB 1.0 版本中,InnoDB 引擎处理 OLTP 的事务型业务,Tianmu 引擎处理 OLAP 的分析型业务。因此,需要在主从复制环境的基础上做读写分离,所有的写操作和部分读操作走 InnoDB 引擎,所有的分析类查询走 Tianmu 引擎。读写分离方案既可以使用第三方中间件,也可以在业务前端实现。本文介绍的是较为常用的中间件 ProxySQL。 服务器配置说明 IP Memory CPU OS version 192.168.30.40 8G 8C CentOS Linux release 7.9 192.168.30.41 8G 8C CentOS Linux release 7.9 192.168.30.42 8G 8C CentOS Linux release 7.9 192.168.30.46 16G 16C CentOS Linux release 7.9 注:主从环境中的各个服务器的配置一般情况下建议是一致的,但由于 StoneDB 不管重放 binlog,还是用于 OLAP 场景的查询,都是较消耗系统资源的,建议 StoneDB 配置略高于 MySQL。 主从环境说明 IP DATABASE ROLE DB version 192.168.30.40 MySQL master MySQL 5.7 192.168.30.41 / ProxySQL / 192.168.30.42 MySQL slave MySQL 5.7 192.168.30.46 StoneDB slave StoneDB 5.7 注:MySQL 与 StoneDB 的版本建议保持一致。 架构图说明 推荐采用一主两从的架构,上层的 ProxySQL 用于读写分离: 1)master(192.168.30.40)使用 InnoDB 引擎,可读写,提供 OLTP 场景的读写业务; 2)slave1(192.168.30.42)使用 InnoDB 引擎,只读,同时作为 standby,当 master 发生宕机时,可切换至 slave1,保证业务正常运行; 3)slave2(192.168.30.46)使用 Tianmu 引擎,只读,提供 OLAP 场景的读业务。 1、操作系统环境检查 操作系统环境检查的步骤在四个节点均需要执行。 1.1 关闭防火墙 # systemctl stop firewalld # systemctl disable firewalld 1.2 关闭SELINUX # vim /etc/selinux/config SELINUX = disabled 1.3 设置Swap分区 修改vm.swappiness的值为1,表示尽量不使用Swap。 # vi /etc/sysctl.conf vm.swappiness = 1 1.4 修改操作系统的限制 # ulimit -a core file size (blocks, -c) 0 data seg size (kbytes, -d) unlimited scheduling priority (-e) 0 file size (blocks, -f) unlimited pending signals (-i) 1031433 max locked memory (kbytes, -l) 64 max memory size (kbytes, -m) unlimited open files (-n) 65535 pipe size (512 bytes, -p) 8 POSIX message queues (bytes, -q) 819200 real-time priority (-r) 0 stack size (kbytes, -s) 10240 cpu time (seconds, -t) unlimited max user processes (-u) 1024 virtual memory (kbytes, -v) unlimited file locks (-x) unlimited 修改操作系统的软硬限制 # vim /etc/security/limits.conf * soft nofile 65535 * hard nofile 65535 mysql soft nproc 1028056 mysql hard nproc 1028056 1.5 创建用户 # groupadd mysql # useradd -g mysql mysql # passwd mysql ProxySQL 节点无需创建,以上步骤执行完之后,重启操作系统。 2、部署MySQL 在 master 节点和 slave1 节点安装 MySQL。 2.1 下载安装包 https://downloads.mysql.com/archives/community/ 从官网下载 MySQL 5.7 的安装包。 2.2 卸载mariadb # rpm -qa|grep mariadb mariadb-5.5.56-2.el7.x86_64 mariadb-server-5.5.56-2.el7.x86_64 mariadb-libs-5.5.56-2.el7.x86_64 # yum remove mariadb* # rpm -qa|grep mariadb 2.3 上传tar包并解压 # tar -zxvf mysql-5.7.36-linux-glibc2.12-x86_64.tar.gz -C /usr/local/ # cd /usr/local/ # mv mysql-5.7.36-linux-glibc2.12-x86_64 mysql 2.4 创建目录 # mkdir -p /mysql/data/ # mkdir -p /mysql/log # chown -R mysql:mysql /mysql/ 2.5 配置参数文件 my.cnf master # vim /etc/my.cnf [client] port = 3306 socket = /mysql/data/mysql.sock [mysqld] port = 3306 basedir = /usr/local/mysql datadir = /mysql/data socket = /mysql/data/mysql.sock pid_file = /mysql/data/mysqld.pid log_error = /mysql/log/mysqld.log log_bin = /mysql/log/mybinlog server_id = 40 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = innodb read_only=0 innodb_buffer_pool_size = 4096000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 binlog_transaction_dependency_tracking = WRITESET transaction_write_set_extraction = XXHASH64 slave1 # vim /etc/my.cnf [client] port = 3306 socket = /mysql/data/mysql.sock [mysqld] port = 3306 basedir = /usr/local/mysql datadir = /mysql/data socket = /mysql/data/mysql.sock pid_file = /mysql/data/mysqld.pid log_error = /mysql/log/mysqld.log log_bin = /mysql/log/mybinlog server_id = 42 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = innodb read_only=1 innodb_buffer_pool_size = 4096000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 4 2.6 初始化实例 /usr/local/mysql/bin/mysqld --defaults-file=/etc/my.cnf --initialize --user=mysql 2.7 启动实例 /usr/local/mysql/bin/mysqld_safe --defaults-file=/etc/my.cnf --user=mysql & 注:管理员用户的临时密码在 mysqld.log 中,第一次登陆后需要修改管理员用户的密码。 3、部署StoneDB 3.1 下载安装包 https://stonedb.io/zh/docs/download/ 从官网下载 StoneDB 5.7 的安装包。 3.2 上传tar包并解压 # cd / # tar -zxvf stonedb-ce-5.7-v1.0.0.el7.x86_64.tar.gz 用户可根据安装规范将安装包上传至服务器,解压出来的目录是 stonedb57,示例中的安装路径是 /stonedb57。 3.3 检查依赖文件 # cd /stonedb57/install/bin # ldd mysqld # ldd mysql 如果检查返回有关键字"not found",说明缺少文件,需要安装对应的依赖包。例如: libsnappy.so.1 => not found 在 Ubuntu 上使用命令 "sudo apt search libsnappy" 检查,说明需要安装 libsnappy-dev。在 RedHat 或者 CentOS 上使用命令 "yum search all snappy" 检查,说明需要安装 snappy-devel、snappy。 3.4 创建目录 mkdir -p /stonedb57/install/data mkdir -p /stonedb57/install/binlog mkdir -p /stonedb57/install/log mkdir -p /stonedb57/install/tmp mkdir -p /stonedb57/install/redolog mkdir -p /stonedb57/install/undolog chown -R mysql:mysql /stonedb57 3.5 配置参数文件 my.cnf # vim /stonedb57/install/my.cnf [client] port = 3306 socket = /stonedb57/install/tmp/mysql.sock [mysqld] port = 3306 basedir = /stonedb57/install/ datadir = /stonedb57/install/data socket = /stonedb57/install/tmp/mysql.sock pid_file = /stonedb57/install/data/mysqld.pid log_error = /stonedb57/install/log/mysqld.log log_bin = /stonedb57/install/binlog/binlog server_id = 46 character_set_server = utf8mb4 collation_server = utf8mb4_general_ci max_connections = 1000 binlog_format = row default_storage_engine = tianmu read_only=1 innodb_buffer_pool_size = 2048000000 innodb_log_file_size = 1024000000 innodb_log_files_in_group = 3 innodb_io_capacity = 4000 innodb_io_capacity_max = 8000 innodb_log_group_home_dir = /stonedb57/install/redolog/ innodb_undo_directory = /stonedb57/install/undolog/ innodb_undo_log_truncate = 1 innodb_undo_tablespaces = 3 innodb_undo_logs = 128 #开启GTID模式 gtid_mode = on enforce_gtid_consistency = 1 #并行复制 slave_parallel_type = LOGICAL_CLOCK slave_parallel_workers = 8 3.6 初始化实例 /stonedb57/install/bin/mysqld --defaults-file=/stonedb57/install/my.cnf --initialize --user=mysql 3.7 启动实例 /stonedb57/install/bin/mysqld_safe --defaults-file=/stonedb57/install/my.cnf --user=mysql & 注:管理员用户的临时密码在 mysqld.log 中,第一次登陆后需要修改管理员用户的密码。 4、配置主从 4.1 创建复制用户 create user 'repl'@'%' identified by 'mysql123'; grant replication slave on *.* to 'repl'@'%'; 4.2 备份主库 /usr/local/mysql/bin/mysqldump -uroot -pmysql123 --single-transaction --set-gtid-purged=on -B aa > /tmp/aa.sql 4.3 传输备份文件 scp /tmp/aa.sql root@192.168.30.42:/tmp scp /tmp/aa.sql root@192.168.30.43:/tmp 注:如果数据较大,建议使用 mydumper. 4.4 slave1节点 /usr/local/mysql/bin/mysql -uroot -pmysql123 -S /mysqldb/data/mysql.sock source /tmp/aa.sql 注:恢复前需要确保 gtid_executed 为空。 4.5 slave2节点 在恢复前,需要修改存储引擎,注释锁表语句。 sed -i 's/UNLOCK TABLES/-- UNLOCK TABLES/g' /tmp/aa.sql sed -i 's/LOCK TABLES `/-- LOCK TABLES `/g' /tmp/aa.sql sed -i 's/ENGINE=InnoDB/ENGINE=tianmu/g' /tmp/aa.sql /stonedb57/install/bin/mysql -uroot -pmysql123 -S /stonedb57/install/tmp/mysql.sock source /tmp/aa.sql 注:恢复前需要确保 gtid_executed 为空。 4.6 建立主从复制 slave1节点 CHANGE MASTER TO MASTER_HOST='192.168.30.40', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='mysql123', MASTER_AUTO_POSITION = 1; start slave; show slave status\G slave2节点 CHANGE MASTER TO MASTER_HOST='192.168.30.40', MASTER_PORT=3306, MASTER_USER='repl', MASTER_PASSWORD='mysql123', MASTER_AUTO_POSITION = 1; start slave; show slave status\G 5、配置ProxySQL 5.1 安装ProxySQL # mkdir -p /home/ProxySQL # cd /home/ProxySQL # yum install proxysql-2.2.0-1-centos7.x86_64.rpm # rpm -qa|grep proxysql # rpm -ql proxysql 注:/etc/proxysql.cnf 为 ProxySQL 的配置文件。 5.2 启动ProxySQL # systemctl start proxysql # netstat -lntp|grep proxysql tcp 0 0 0.0.0.0:6032 0.0.0.0:* LISTEN 17957/proxysql tcp 0 0 0.0.0.0:6033 0.0.0.0:* LISTEN 17957/proxysql 注:6032是管理端口,6033是服务端口。 5.3 管理员登录ProxySQL # mysql -uadmin -padmin -h127.0.0.1 -P6032 --prompt='ProxySQL>' mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 1 Server version: 5.5.30 (ProxySQL Admin Module) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. ProxySQL>show databases; +-----+---------------+-------------------------------------+ | seq | name | file | +-----+---------------+-------------------------------------+ | 0 | main | | | 2 | disk | /var/lib/proxysql/proxysql.db | | 3 | stats | | | 4 | monitor | | | 5 | stats_history | /var/lib/proxysql/proxysql_stats.db | +-----+---------------+-------------------------------------+ 5 rows in set (0.00 sec) 注:ProxySQL 的用户名和密码默认都是 admin,加参数 prompt 是为了便于区分环境。 5.4 创建ProxySQL所需用户 在 master 上创建 ProxySQL 的监控用户和对外访问用户,其中监控用户只需要有 replication client 权限即可。 ###监控用户 create user 'monitor'@'%' identified by 'MySQL_123'; grant replication client on *.* to 'monitor'@'%'; ###对外访问用户(用于连接ProxySQL) create user 'proxysql'@'%' identified by 'MySQL_123'; grant select,delete,update,insert on *.* to 'proxysql'@'%'; 5.5 配置ProxySQL主从分组信息 1)创建分组 表 mysql_replication_hostgroups 的字段 writer_hostgroup、reader_hostgroup 分别代表写组和读组,都要大于0且不能相同,该环境中定义写组为10,读组为20。 ProxySQL 会根据 read_only 的取值将 server 进行分组,read_only=0为 master,被分到编号为10的写组,read_only=1为 slave,被分到编号为20的读组。 ###创建分组 insert into mysql_replication_hostgroups(writer_hostgroup,reader_hostgroup,comment) values(10,20,'proxy'); load mysql servers to runtime; save mysql servers to disk; ###查看三层配置系统是否都写入数据 ProxySQL>select * from mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) ProxySQL>select * from runtime_mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) ProxySQL>select * from mysql_replication_hostgroups; +------------------+------------------+------------+---------+ | writer_hostgroup | reader_hostgroup | check_type | comment | +------------------+------------------+------------+---------+ | 10 | 20 | read_only | proxy | +------------------+------------------+------------+---------+ 1 row in set (0.00 sec) 2)添加主从节点 insert into mysql_servers(hostgroup_id,hostname,port) values(10,'192.168.30.40',3306); insert into mysql_servers(hostgroup_id,hostname,port) values(20,'192.168.30.46',3306); load mysql servers to runtime; save mysql servers to disk; ###查看状态 ProxySQL>select * from mysql_servers; +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ | hostgroup_id | hostname | port | gtid_port | status | weight | compression | max_connections | max_replication_lag | use_ssl | max_latency_ms | comment | +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ | 20 | 192.168.30.40 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | | | 20 | 192.168.30.46 | 3306 | 0 | ONLINE | 1 | 0 | 1000 | 0 | 0 | 0 | | +--------------+---------------+------+-----------+--------+--------+-------------+-----------------+---------------------+---------+----------------+---------+ 2 rows in set (0.00 sec) 3)为ProxySQL创建监控用户 ###创建监控用户(在ProxySQL创建) set mysql-monitor_username='monitor'; set mysql-monitor_password='MySQL_123'; load mysql variables to runtime; save mysql variables to disk; ###对连接用户监控 ProxySQL>select * from monitor.mysql_server_connect_log; +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ | hostname | port | time_start_us | connect_success_time_us | connect_error | +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ | 192.168.30.40 | 3306 | 1664183920198998 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183920926981 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.46 | 3306 | 1664183970671663 | 1616 | NULL | | 192.168.30.40 | 3306 | 1664183971672625 | 2089 | NULL | +---------------+------+------------------+-------------------------+------------------------------------------------------------------------+ 4 rows in set (0.00 sec) 注:在没有创建监控用户前,会有很多的connect_error,这是因为没有配置监控信息时的错误,配置后如果connect_error的结果为NULL,则表示正常。 ###对心跳信息的监控 ProxySQL>select * from mysql_server_ping_log limit 10; +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ | hostname | port | time_start_us | ping_success_time_us | ping_error | +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ | 192.168.30.40 | 3306 | 1664183880229349 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183880427787 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183890229405 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183890336793 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183900229529 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183900357491 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.46 | 3306 | 1664183910229710 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183910406115 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183920229740 | 0 | Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183920346638 | 0 | Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | +---------------+------+------------------+----------------------+------------------------------------------------------------------------+ 10 rows in set (0.00 sec) ###对read_only值监控 ProxySQL>select * from mysql_server_read_only_log limit 10; +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ | hostname | port | time_start_us | success_time_us | read_only | error | +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ | 192.168.30.46 | 3306 | 1664183876942878 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183876961694 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183878441697 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183878461063 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183879941587 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183879961993 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183881441750 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | | 192.168.30.40 | 3306 | 1664183881461890 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.40 | 3306 | 1664183882942044 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'192.168.30.41' (using password: YES) | | 192.168.30.46 | 3306 | 1664183882958866 | 0 | NULL | timeout on creating new connection: Access denied for user 'monitor'@'HAMI02' (using password: YES) | +---------------+------+------------------+-----------------+-----------+------------------------------------------------------------------------------------------------------------+ 10 rows in set (0.00 sec) 注:monitor就会开始监控后端的read_only值,然后按照read_only的值将某些节点自动移到读写组。 4)为ProxySQL配置对外访问用户 insert into mysql_users(username,password,default_hostgroup) values('proxysql','MySQL_123',10); insert into mysql_users(username,password,default_hostgroup) values('proxysql2','*0815E74A768849A6CCF0E9C1C5B940FB4D9F839E',20); load mysql users to runtime; save mysql users to disk; ProxySQL>select * from mysql_users\G *************************** 1. row *************************** username: proxysql password: MySQL_123 active: 1 use_ssl: 0 default_hostgroup: 10 default_schema: NULL schema_locked: 0 transaction_persistent: 1 fast_forward: 0 backend: 1 frontend: 1 max_connections: 10000 attributes: comment: *************************** 2. row *************************** username: proxysql2 password: *0815E74A768849A6CCF0E9C1C5B940FB4D9F839E active: 1 use_ssl: 0 default_hostgroup: 20 default_schema: NULL schema_locked: 0 transaction_persistent: 1 fast_forward: 0 backend: 1 frontend: 1 max_connections: 10000 attributes: comment: 2 rows in set (0.00 sec) mysql_users 表最主要的三个字段 username、password、default_hostgroup 解释。 username:前端连接 ProxySQL 的数据库用户 password:用户对应的密码,即可以是明文密码,也可以是 hash 密码,如果想使用 hash 密码,可以先在某个节点上执行 select password('password'),然后将加密结果复制到该字段。 default_hostgroup:用户默认的路由目标,例如:若用户 proxysql2 是个只读用户,则该字段值可以设置为20,表示所有的SQL语句默认情况下将路由到 hostgroup_id=20 的组;若用户不是只读用户,则该字段必须设置为10,表示 DML 语句会路由到 hostgroup_id=10 的组,查询语句即会路由到 hostgroup_id=10 的组,也会路由到 hostgroup_id=20 的组。 5.6 配置读写分离策略 ProxySQL 的路由规则配置比较灵活,可以基于用户级别,数据库级别等。由于 StoneDB 提供的是 OLAP 分析型查询业务场景,建议将聚合类查询、即席查询、复杂查询等分发到 StoneDB。由于只是测试,因此只配置了几个简单的路由规则。 与查询规则有关的表有两个:mysql_query_rules 和 mysql_query_rules_fast_routing,表mysql_query_rules_fast_routing 是 mysql_query_rules 的扩展,并在以后评估快速路由策略和属性(仅在ProxySQL 1.4.7+中可用)。 mysql_query_rules 表的几个字段解释。 active:是否启用这个规则,1表示启用,0表示禁用 match_pattern:设置规则 destination_hostgroup:默认指定的分组 apply:真正执行应用规则 ###创建规则 这里我创建两个规则: 1)把所有以select开头的语句分配到编号为20的读组中; 2)把select...for update语句分配到编号为10的写组中,其他所有操作都会默认路由到写组。 insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(1,1,'^select.*for update$',10,1); insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(2,1,'^select',20,1); insert into mysql_query_rules(rule_id,active,match_pattern,destination_hostgroup,apply) values(3,1,'^select*sum',20,1); load mysql query rules to runtime; save mysql query rules to disk; ###查看规则 ProxySQL>select * from mysql_query_rules\G *************************** 1. row *************************** rule_id: 1 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select.*for update$ negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 10 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL *************************** 2. row *************************** rule_id: 2 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 20 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL *************************** 3. row *************************** rule_id: 3 active: 1 username: NULL schemaname: NULL flagIN: 0 client_addr: NULL proxy_addr: NULL proxy_port: NULL digest: NULL match_digest: NULL match_pattern: ^select*sum negate_match_pattern: 0 re_modifiers: CASELESS flagOUT: NULL replace_pattern: NULL destination_hostgroup: 20 cache_ttl: NULL cache_empty_result: NULL cache_timeout: NULL reconnect: NULL timeout: NULL retries: NULL delay: NULL next_query_flagIN: NULL mirror_flagOUT: NULL mirror_hostgroup: NULL error_msg: NULL OK_msg: NULL sticky_conn: NULL multiplex: NULL gtid_from_hostgroup: NULL log: NULL apply: 1 attributes: comment: NULL 3 rows in set (0.00 sec) 注:select...for update 规则的 rule_id 必须要小于普通的 select 规则的 rule_id,因为 ProxySQL 是根据 rule_id的顺序进行规则匹配的。 5.7 读写分离测试 1)读操作 # mysql -uproxysql -pMySQL_123 -h127.0.0.1 -P6033 mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 6 Server version: 5.5.30 (ProxySQL) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. proxysql@HAMI02 17:21: [(none)]> select sum(money) from aa.ttt; +------------+ | sum(money) | +------------+ | 88888.8 | +------------+ 1 row in set (0.01 sec) proxysql@HAMI02 17:21: [(none)]> select @@server_id; +-------------+ | @@server_id | +-------------+ | 46 | +-------------+ 1 row in set (0.00 sec) 2)写操作 # mysql -uproxysql -pMySQL_123 -h127.0.0.1 -P6033 mysql: [Warning] Using a password on the command line interface can be insecure. Welcome to the MySQL monitor. Commands end with ; or \g. Your MySQL connection id is 19 Server version: 5.5.30 (ProxySQL) Copyright (c) 2000, 2021, Oracle and/or its affiliates. Oracle is a registered trademark of Oracle Corporation and/or its affiliates. Other names may be trademarks of their respective owners. Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. proxysql@HAMI02 19:53: [(none)]> begin; Query OK, 0 rows affected (0.00 sec) proxysql@HAMI02 19:54: [(none)]> insert into aa.t1 values(7); Query OK, 1 row affected (0.00 sec) proxysql@HAMI02 19:54: [(none)]> select @@server_id; +-------------+ | @@server_id | +-------------+ | 40 | +-------------+ 1 row in set (0.00 sec) 6、建议项 1)建议设置为 GTID 模式,好处是从库发生 crash 后,不需要去找位点,系统会自动拉起复制线程; 2)在从库的并行 worker 相同的情况下,从库 StoneDB 相比从库 InnoDB 会消耗更多的CPU资源,如果未来上生产环境,建议 StoneDB 的配置略比 InnoDB 的配置高; 3)若从库 StoneDB 的延迟较高,可临时关闭参数 tianmu_enable_rowstore,待追上主库后再开启参数。关闭该参数带来的风险是如果在同步过程中 StoneDB 发生重启,重启后会出现丢数据的情况; 4)建议将聚合类查询、即席查询、复杂查询等分发到 StoneDB。

优秀的个人博客,低调大师

Optional源码解析与实践

1 导读 NullPointerException在开发过程中经常遇到,稍有不慎小BUG就出现了,如果避免这个问题呢,Optional就是专门解决这个问题的类,那么Optional如何使用呢?让我们一起探索一下吧! 2 源码解析 2.1 Optional定义 Optional类是Java8为了解决null值判断问题而创建的容器类,在java.util 下,使用Optional类可以避免显式的null值判断,避免null导致的NullPointerException。首先,Optional是一个容器,它可以保存类型T的值,也可以为null的容器对象。Optional容器只能存一个值。 2.2 Optional的属性 1)源码: /** * Common instance for {@code empty()}. */ private static final Optional<?> EMPTY = new Optional<>(); /** * If non-null, the value; if null, indicates no value is present */ private final T value; 根据源码可以看到Optional有两个属性,一个是为空值准备的EMPTY和泛型值value; 2.3 Optional的方法 Optional除toString()、hashCode() 、equals()等Object的方法外,还包含以下方法。 2.3.1 私有构造方法 /** * Constructs an empty instance. * * @implNote Generally only one empty instance, {@link Optional#EMPTY}, * should exist per VM. */ private Optional() { this.value = null; } /** * Constructs an instance with the value present. * * @param value the non-null value to be present * @throws NullPointerException if value is null */ private Optional(T value) { this.value = Objects.requireNonNull(value); } 分别是创建一个空实例和构造一个具有当前值的实例。 2.3.2 创建方法 1)源码 public static<T> Optional<T> empty() { @SuppressWarnings("unchecked") Optional<T> t = (Optional<T>) EMPTY; return t; } public static <T> Optional<T> of(T value) { return new Optional<>(value); } public static <T> Optional<T> ofNullable(T value) { return value == null ? empty() : of(value); } 2)方法说明 empty(): 创建一个空的 Optional 实例 of(T t) : 创建一个 Optional 实例,当 t为null时抛出异常 ofNullable(T t): 创建一个 Optional 实例,但当 t为null时不会抛出异常,而是返回一个空的实例 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; try { Optional<Integer> optional1 = Optional.empty(); System.out.println("optional1创建了"); }catch (Exception e){ System.out.println("optional1失败了"); } try { Optional<Integer> optional2 = Optional.of(value1); System.out.println("optional2创建了"); }catch (Exception e){ System.out.println("optional2失败了"); } try { Optional<Integer> optional3 = Optional.ofNullable(value1); System.out.println("optional3创建了"); }catch (Exception e){ System.out.println("optional3失败了"); } try { Optional<Integer> optional4 = Optional.of(value2); System.out.println("optional4创建了"); }catch (Exception e){ System.out.println("optional4失败了"); } try { Optional<Integer> optional5 = Optional.ofNullable(value2); System.out.println("optional5创建了"); }catch (Exception e){ System.out.println("optional5失败了"); } } 4)运行结果 2.3.3 值获取方法 1)源码 public T get() { if (value == null) { throw new NoSuchElementException("No value present"); } return value; } 2)方法说明 get(): 如果Optional不为空,则返回该Optional容器中的值,否则抛出NoSuchElementExceptio 。 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); try { Integer result=optional1.get(); System.out.println("optional1的值是:"+result); }catch (Exception e){ System.out.println("optional1的值获取失败,原因:"+e.getMessage()); } try { Integer result=optional2.get(); System.out.println("optional2的值是:"+result); }catch (Exception e){ System.out.println("optional2的值获取失败,原因:"+e.getMessage()); } } 4)运行结果 2.3.4 判断方法 1)源码 public boolean isPresent() { return value != null; } public void ifPresent(Consumer<? super T> consumer) { if (value != null) consumer.accept(value); } public T orElse(T other) { return value != null ? value : other; } public T orElseGet(Supplier<? extends T> other) { return value != null ? value : other.get(); } public <X extends Throwable> T orElseThrow(Supplier<? extends X> exceptionSupplier) throws X { if (value != null) { return value; } else { throw exceptionSupplier.get(); } } 2)方法说明 isPresent(): 判断optional是否为空,如果空则返回false,否则返回true ifPresent(Consumer c): 如果optional不为空,则将optional中的对象传给Comsumer函数 orElse(T other): 如果optional不为空,则返回optional中的对象;如果为null,则返回 other 这个对象。 orElseGet(Supplier other): 如果optional不为空,则返回optional中的对象;如果为null,否则调用其他函数并返回调用的结果 orElseThrow(Supplier exception): 如果optional不为空,则返回optional中的对象;如果为null,则抛出Supplier函数生成的异常 3)测试代码 public static void main(String[] args) { Integer value1 = null; Integer value2 = 1; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); try { if(optional1.isPresent()){ System.out.println("optional1的isPresent结果不为空"); }else{ System.out.println("optional1的isPresent结果为空"); } }catch (Exception e){ System.out.println("optional1的isPresent判空失败,原因:"+e.getMessage()); } try { if(optional2.isPresent()){ System.out.println("optional2的isPresent结果不为空"); }else{ System.out.println("optional2的isPresent结果为空"); } }catch (Exception e){ System.out.println("optional2的isPresent判空失败,原因:"+e.getMessage()); } optional1.ifPresent(t->{ int i =t+1; System.out.println("optional1处理后的值是"+i); }); optional2.ifPresent(t->{ int i =t+1; System.out.println("optional2处理后的值是"+i);}); Integer value3 = 2; Integer result = optional1.orElse(value3); System.out.println("optional1执行orElse处理后的值是"+result); result = optional2.orElse(value3); System.out.println("optional2执行orElse处理后的值是"+result); result = optional1.orElseGet(()-> new Integer(-1)); System.out.println("optional1执行orElseGet处理后的值是"+result); result = optional2.orElseGet(()-> new Integer(-1)); System.out.println("optional2执行orElseGet处理后的值是"+result); try { result = optional1.orElseThrow (()-> new RuntimeException("值是空的")); System.out.println("optional1执行orElseThrow处理后的值是"+result); }catch (Exception e){ System.out.println("optional1的orElseThrow抛出异常:"+e.getMessage()); } try { result = optional2.orElseThrow (()-> new RuntimeException("值是空的")); System.out.println("optional2执行orElseThrow处理后的值是"+result); }catch (Exception e){ System.out.println("optional2的orElseThrow抛出异常:"+e.getMessage()); 4)运行结果 2.3.5 过滤方法 1)源码 public Optional<T> filter(Predicate<? super T> predicate) { Objects.requireNonNull(predicate); if (!isPresent()) return this; else return predicate.test(value) ? this : empty(); } 2)方法说明 filter(Predicate p): 如果optional不为空,则执行Predicate p,如果p的结果为true,则返回原本的optional,否则返回空的optional 3)测试代码 public static void main(String[] args) { Integer value1 = 5; Integer value2 = 6; Optional<Integer> optional1 = Optional.ofNullable(value1); Optional<Integer> optional2 = Optional.of(value2); Optional<Integer> result =optional1.filter(t->t > 5); System.out.println("optional1的filter后的值:"+result); result =optional2.filter(t->t > 5); System.out.println("optional2的filter后的值:"+result); 4)运行结果 2.3.6 映射方法 1)源码 public<U> Optional<U> map(Function<? super T, ? extends U> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Optional.ofNullable(mapper.apply(value)); } } public<U> Optional<U> flatMap(Function<? super T, Optional<U>> mapper) { Objects.requireNonNull(mapper); if (!isPresent()) return empty(); else { return Objects.requireNonNull(mapper.apply(value)); } } 2)方法说明 map(Function mapper): 如果存在一个值,则对其应用提供的映射函数,如果结果非空,则返回描述结果的Optional。 否则返回一个空的Optional。 flatMap(Function< T,Optional> mapper): 如果有值,则对其应用提供的可选映射函数,返回结果,否则返回空的可选函数。 这个方法类似于map(Function),但是提供的映射器的结果已经是一个可选的,如果调用,flatMap不会用额外的可选的包装它。 区别:map会自动将u放到optional中,而flatMap则需要手动给u创建一个optional 3)测试代码 public static void main(String[] args) { User user1 = null; User user2 = new User("user2名字",19); Optional<User> optional1 = Optional.ofNullable(user1); Optional<User> optional2 = Optional.of(user2); System.out.println("=========map=========="); System.out.println("optional1的map前的值:"+optional1); Optional<String> result =optional1.map(t->t.getName()); System.out.println("optional1的map后的值:"+result); System.out.println("optional2的map前的值:"+optional2); result =optional2.map(t->t.getName()); System.out.println("optional2的map后的值:"+result); System.out.println("===========flatMap========"); System.out.println("optional1的flatMap前的值:"+optional1); Optional<Integer> result2 =optional1.flatMap(t->Optional.ofNullable(t.getAge())); System.out.println("optional1的flatMap后的值:"+result2); System.out.println("optional2的flatMap前的值:"+optional2); result2 =optional2.flatMap(t->Optional.ofNullable(t.getAge())); System.out.println("optional2的flatMap后的值:"+result2); } public class User { String name; Integer age; public User(String name,Integer age){ this.name = name; this.age=age; } public String getName() { return name; } public Integer getAge() { return age; 4)运行结果 3 应用实例 3.1 错误用法 由于Optional并没有实现Serializable接口,所以不能作为类的属性。 不要把Optional作为方法的参数。 把if(x!=null)直接换成Optional.ofNullable(x).isPresent(),这样有过度编码的嫌疑。 直接使用Optional.get()的返回值进行操作,String result =Optional.ofNullable(null).get().toString();这样还是会抛出异常的。 3.2 建议用法 A类有属性B类,B类有属性C类,C类有name这个字段。 使用Optional之前: if(atest!=null){ Btest btest =atest.getBtest(); if(btest!=null){ Ctest ctest = btest.getCtest(); if (ctest != null) { name =ctest.getName(); } } } 使用Optional之后: name = Optional.ofNullable(atest).map(t->t.getBtest()).map(t->t.getCtest()).map(t->t.getName()).orElse("默认值"); 代码是不是看上去更整洁了呢? 4 总结 通过对Optional源码解析和用例测试代码的运行结果,可以看出使用Optional可以优化null值判断代码,让代码变得更加优雅和整洁。 作者:陈昌浩

资源下载

更多资源
腾讯云软件源

腾讯云软件源

为解决软件依赖安装时官方源访问速度慢的问题,腾讯云为一些软件搭建了缓存服务。您可以通过使用腾讯云软件源站来提升依赖包的安装速度。为了方便用户自由搭建服务架构,目前腾讯云软件源站支持公网访问和内网访问。

Nacos

Nacos

Nacos /nɑ:kəʊs/ 是 Dynamic Naming and Configuration Service 的首字母简称,一个易于构建 AI Agent 应用的动态服务发现、配置管理和AI智能体管理平台。Nacos 致力于帮助您发现、配置和管理微服务及AI智能体应用。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据、流量管理。Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。

Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册