「深度学习福利」大神带你进阶工程师,立即查看>>>
概述
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。 ACID
事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。 隔离级别
ACID这4个特征中,最难理解的是隔离性。在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。4个隔离级别分别是:读未提及(READ_UNCOMMITTED)、读已提交(READ_COMMITTED)、可重复读(REPEATABLE_READ)、顺序读(SERIALIZABLE)。 事务并发引起的问题
数据库在不同的隔离性级别下并发访问可能会出现以下几种问题:脏读(Dirty Read)、不可重复读(Unrepeatable Read)、幻读(Phantom Read)。
事务的思维导图
ACID
1. 原子性(Atomicity)
事务的原子性是指事务必须是一个原子的操作序列单元。事务中包含的各项操作在一次执行过程中,只允许出现两种状态之一。 全部执行成功 全部执行失败
任何一项操作都会导致整个事务的失败,同时其它已经被执行的操作都将被撤销并回滚,只有所有的操作全部成功,整个事务才算是成功完成。
2. 一致性(Consistency)
事务的一致性是指事务的执行不能破坏数据库数据的完整性和一致性,一个事务在执行之前和执行之后,数据库都必须处以一致性状态。
比如:如果从A账户转账到B账户,不可能因为A账户扣了钱,而B账户没有加钱。
3. 隔离性(Isolation)
事务的隔离性是指在并发环境中,并发的事务是互相隔离的,一个事务的执行不能被其它事务干扰。也就是说,不同的事务并发操作相同的数据时,每个事务都有各自完整的数据空间。
一个事务内部的操作及使用的数据对其它并发事务是隔离的,并发执行的各个事务是不能互相干扰的。
隔离性分4个级别,下面会介绍。
4. 持久性(Duration)
事务的持久性是指事务一旦提交后,数据库中的数据必须被永久的保存下来。即使服务器系统崩溃或服务器宕机等故障。只要数据库重新启动,那么一定能够将其恢复到事务成功结束后的状态。
事务隔离级别
1. 读未提及(READ_UNCOMMITTED)
读未提及,该隔离级别允许脏读取,其隔离级别是最低的。换句话说,如果一个事务正在处理某一数据,并对其进行了更新,但同时尚未完成事务,因此还没有提交事务;而以此同时,允许另一个事务也能够访问该数据。
脏读示例:
在事务A和事务B同时执行时可能会出现如下场景:
时间 事务A(存款) 事务B(取款)
T1 开始事务 —— T2 —— 开始事务 T3 —— 查询余额(1000元) T4 —— 取出1000元(余额0元) T5 查询余额(0元) —— T6 —— 撤销事务(余额恢复1000元) T7 T8 存入500元(余额500元) 提交事务 —— ——
余额应该为1500元才对。请看T5时间点,事务A此时查询的余额为0,这个数据就是 脏数据 ,他是事务B造成的,很明显是事务没有进行隔离造成的。 2. 读已提交(READ_COMMITTED) 读已提交是不同的时候执行的时候只能获取到已经提交的数据。 这样就不会出现上面的脏读的情况了。 不可重复读示例 可是解决了脏读问题,但是还是解决不了可重复读问题。 时间 事务A(存款) 事务B(取款) T1 开始事务 —— T2 —— 开始事务 T3 —— 查询余额(1000元) T4 查询余额(1000元) —— T5 —— 取出1000元(余额0元) T6 —— 提交事务 T7 T8 查询余额(0元) 提交事务 —— ——
事务A其实除了查询两次以外,其它什么事情都没做,结果钱就从1000编程0了,这就是不可重复读的问题。 3. 可重复读(REPEATABLE_READ) 可重复读就是保证在事务处理过程中,多次读取同一个数据时,该数据的值和事务开始时刻是一致的。因此该事务级别进制了不可重复读取和脏读,但是有可能出现幻读的数据。 幻读 幻读就是指同样的事务操作,在前后两个时间段内执行对同一个数据项的读取,可能出现不一致的结果。 时间 事务A(统计总存款) 事务B(存款) T1 开始事务 —— T2 —— 开始事务 T3 统计总存款(1000元) —— T4 —— 存入100元 T5 —— 提交事务 T6 T7 提交总存款(10100) 提交事务 —— ——
银行工作人员在一个事务中多次统计总存款时看到结果不一样。如果要解决幻读,那只能使用顺序读了。 4. 顺序读(SERIALIZABLE) 顺序读是最严格的事务隔离级别。它要求所有的事务排队顺序执行,即事务只能一个接一个地处理,不能并发。 事务隔离级别对比 事务隔离级别 脏 读 不可重复读 幻 读 读未提及(READ_UNCOMMITTED) 允许 允许 允许 读已提交(READ_COMMITTED) 可重复读(REPEATABLE_READ) 顺序读(SERIALIZABLE) 禁止 禁止 禁止 允许 禁止 禁止 允许 允许 禁止 4种事务隔离级别从上往下,级别越高,并发性越差,安全性就越来越高。 一般数据默认级别是读以提交或可重复读。 作者:jijs 链接:https://www.jianshu.com/p/aa35c8703d61 来源:简书 简书著作权归作者所有,任何形式的转载都请联系作者获得授权并注明出处。 「深度学习福利」大神带你进阶工程师,立即查看>>> 过去一年在 PingCAP 全力奔跑的同时,越来越多的小伙伴开始关注我们、了解我们,我们的团队也愈加庞大,我们也期待更多对我们感兴趣的小伙伴加入我们,跟我们一起做点有意义的事情。可能有些小伙伴对我司「神秘的招聘职位」感到茫然,对我们在做的事情也没有深入的了解,于是我们准备推出「PingCAP 招聘职位深度解读」系列文章,介绍 PingCAP 各个团队的小伙伴们现在在做什么、接下来的规划是什么、不同团队吸纳成员的核心需求是什么等等。 本篇将带大家速览我司各个研发团队的定位和分工,并回答一个热门问题「在 PingCAP 工作是什么样的体验?」 作为开源的新型分布式数据库公司,PingCAP 一直致力于探索并逐步解决分布式数据库领域的诸多问题 ,比如: 如何设计和实现世界前沿的分布式 SQL 优化器,让一个复杂的 SQL 查询变的无比轻快智能; 如何实现一致性同步的行列格式混合的 HTAP 架构,且 AP 业务对 TP 业务几乎无干扰; 如何在成千上万台集群规模的情况下,实现无阻塞的表结构变更操作,而不影响任何在线的业务; 如何实现高效的分布式事务算法,让 ACID 事务在大规模并发的分布式存场景下依然可以高效可靠; 如何基于 Raft 协议实现快速稳定的数据强一致复制和自动故障恢复,确保数据安全; 如何设计一个高效智能的调度器,负责对上百 TB 的数据进行调度,保证系统平稳运行; 如何在一个 PR 提交之后,让千万级的测试 cases 在三分钟内跑完,并立即看到对数据库性能有没有显著的提升,以及混沌工程的具体实践; 如何在 AWS,GCP,Aliyun 等公有云上一键启动 TiDB 集群,一键伸缩上百个数据库节点,理解有状态服务在 K8s 上调度的最佳实践。 我们研发团队的定位和分工与以上问题息息相关,或者说,是围绕着 TiDB 产品展开的 。
TiDB 产品架构 从上图可以看到,TiDB 集群主要包括三个核心组件:TiDB Server,TiKV Server 和 PD Server,分别用于解决计算、存储、调度这三个核心问题。此外,还有用于解决用户复杂 OLAP 需求的 TiSpark / TiFlash 组件。与之对应的,我们的内核研发团队分别是: TiDB 团队、 TiKV 团队和 AP (Analytical Product)团队,此外还有 Cloud 团队、 EE (Efficiency Engineering)团队和很重要的 QA (Quality Assurance)团队。 所以很多对 TiDB 不太了解的小伙伴看完我们的招聘页面,可能会觉得那些五(没)花(听)八(说)门(过)的研发类职位是特别神秘的存在……吧…… 招聘页面上一小部分神秘部队 那么这些「神秘」团队到底是做什么的? 下面就简单的介绍一下这些研发团队是做什么的吧。 TiDB 团队 TiDB 团队负责所有和 SQL 计算相关的工作以及和客户端(业务)之间的交互,包括协议解析、语法解析、查询优化、执行计算等等,这是一个承上启下的核心模块。除此之外还包括与其他数据库之间的数据迁移和同步组件,比如 TiDB 自身的 Binlog 模块以及读取 MySQL 之类数据源 Binlog 的组件。 TiKV 团队 TiKV 是一个支持事务的,数据强一致的分布式 Key-Value 存储引擎。 从产品架构图中可以看出:无论是 TiDB Server 还是 TiSpark 组件,都是从 TiKV 存取数据的,所以我们一定要保证 TiKV 的稳定和高效。TiKV 团队主要负责的就是分布式 Key-Value 存储引擎的设计和开发,分布式调度系统的设计与研发,构建分布式压力测试框架,稳定性测试框架等工作。 AP 团队 这个是一个比较新的团队,主要负责 OLAP 业务相关的产品,包括之前已经有的 TiSpark 和正在研发中的 AP 扩展引擎 TiFlash 产品。TiDB 是一款 HTAP 的产品,而加强和补齐 HTAP 中的 AP 环节主要就这个组的责任,这里包含了基于 Raft 的一致性同步列存引擎,MPP 计算引擎开发以及大数据相关产品的整合等工作。 Cloud 团队 TiDB 是一个 Cloud Native 的数据库,Cloud 团队的职责就是让 TiDB 更平滑、以更大的规模跑在云上。他们将 TiDB 的组件容器化,并借助 Kubernetes 进行编排与调度。其核心是 TiDB-Operator,实现了云上的快速部署、一键伸缩和故障自治愈。编排有状态的分布式服务是 Kubernetes 最有挑战的事情之一,也是这个团队最擅长解决的问题。Cloud 团队正在努力将 TiDB 构建成为一个云上的服务,即一个 Multi-tenant, Across-cloud, Fully-managed 的 DBaaS(Database as a Service)产品。 EE 团队 这是一个非常 Hack 的团队,致力于解决研发、测试、交付、甚至公司运营中的各种效率问题。他们信仰自动化,摒弃重复性的人工劳动,发明各种 bot 帮助提高 DevOps 的效率;他们创造了强大的“薛定谔”测试平台,将混沌工程变成现实,不断挑战分布式数据库的极限;他们深入系统内核,改造 bcc/eBPF 这些最酷的工具,将操作系统的秘密暴露无遗;他们高效率定位线上的各种疑难杂症,还第一手玩到 Optane Memory 硬件——他们就是神秘的 EE 团队。 QA 团队 每个发布的 TiDB 版本,都有数千万的测试用例来保障产品在客户生产环境下的完美工作。QA 团队开发测试工具和自动化测试框架,并引入混沌工程、人工智能技术来保障 TiDB 的数据一致性和稳定性。 后续我们将每周更新 1-2 篇文章为大家详细介绍以上团队和相关职位。如果大家对文章有意见或建议,欢迎在微信后台留言或者发邮件到 hire@pingcap.com 告诉我们~ 在 PingCAP 工作是什么样的体验? 这可能是很多小伙伴们最最关心的 Part。弹性工作制、零食水果、六险一金这些就不多说了,应该已经成为很多公司的标配,我们来说点有特色的: 工作内容 选择一份工作,工作内容是否有意义、有价值,你是否有兴趣投入其中,这两点至关重要。 在 PingCAP,你可以亲自参与打造一款代表未来数据库产品,接触核心的分布式关系数据库技术,你的每一个想法都会被重视,每一次提交都有可能给整个产品带来意想不到的变化 。 工作伙伴 他们大多来自于国内外一线互联网公司,有非常出色的技术实力,作为聪明人的你一定也想和聪明的人一起工作。 团队成员整体比较年轻,氛围相对轻松、自在。在这里,你可以保留自己的个性和兴趣爱好 。无论你是爱好桌游、喜欢摇滚、热爱运动,都能找到与你志同道合的小伙伴,在从事喜欢的工作的同时也可以做你自己,是不是很 Cool? 开源文化 我们有着 活跃的开源社区 。截止到 2019 年 3 月 1 日,TiDB+TiKV 项目在 GitHub 上的 Star 数已经达到了 21000+,拥有 350+ Contributor,社区的力量在不断壮大。TiDB-Operator、TiDB-DM、TiDB-Lightning 等生态工具陆续开源;24 篇 TiDB 源码阅读系列文章 已经完结, TiKV 源码解析系列文章 已经启动 ;除了开放的线下 Infra Meetup,我们也将内部的 Paper Reading 活动放到了线上直播平台(Bilibili ID: TiDB_Robot)…… 想要了解 2018 年 TiDB 社区的成长足迹可以查看这篇文章—— 《2018 TiDB 社区成长足迹与小红花 | TiDB DevCon 2019》 工作地点 目前除北京总部之外,我们在 上海 、 杭州 、 广州 、 深圳 、 成都 、 硅谷 都设立了 Office。你可以去体验北上广深的快节奏,感受经济、文化、思想的强烈碰撞,也可以去杭州、成都,在下班或午后享受片刻的宁静与悠闲,还可以去硅谷体验前沿的技术氛围;如果你喜欢美食,可以去魔都的人民广场吃炸鸡,也可以去广州品味一下正宗的粤式茶点,还可以去硅谷 Office 尝一尝正宗的西餐,当然还有成都的火锅、小酒馆等着你;甚至你还有机会 Remote 在家,事业家庭两相宜。 需要特别说明的是,我们并不会按照工作地点来划分工作模块 ,每一个 Office 的小伙伴都在我们的核心研发模块中承担着重要角色,而且内部的跨团队和跨地域 Transfer 都非常透明,PingCAP 的整个项目协作也都是分布式的。 全方面的成长 入职之后,Mentor 会为你定制化培养方案,你对于所从事模块的认知会日渐深入,公司内部小伙伴的分享以及 Paper Reading、Meetup 等活动也能够帮助你对于其他知识领域有更加深刻的认识; 公司为每一位小伙伴提供了分享平台,支持并鼓励大家积极分享自己的想法和见解,在这个过程中,你的语言表达能力、逻辑思维能力也能得到一定程度的提升; 当然,如果你具备了作为 Mentor 的能力并有意向尝试 Mentor 的角色,在 PingCAP,都有机会实现。 我们一直以来的理念是希望每个 PingCAP 的小伙伴都先得到个人成长,再反哺给团队和公司,每一个小伙伴都能参与到公司发展的过程中来。我们完全不担心「把你锻炼出来,却被其他公司高价挖走了」这类事情。且不说我们的薪酬本身就很有竞争力,更重要的是,我们相信一旦你喜欢上我们的理念和工作模式,你是不会舍得离开的~ 加入我们吧! 我们认为优秀的工程师或多或少有以下共同特质: A Quick Learner An Earnest Curiosity Faith in Open Source Self-driven Get Things Done 如果你符合以上特质,欢迎进入招聘页面查看目前开放的工作机会: https://www.pingcap.com/recruit-cn/join/#positions 简历投递通道:hire@pingcap.com 实习生 :公司的各项福利和学习资源对实习生全面开放,更重要的是实习生还未毕业就有机会接触工业级项目,而且实习期间表现优异者将有机会获得校招绿色通道特权。如果小伙伴们时间不够充裕,也可以先从社区 Contributor 做起,或许下一期 Talent Plan 的主角就是你! 伯乐推荐 :如果你身边有符合以上要求的小伙伴,也可以找我们聊一聊,推荐成功就有机会获得伯乐推荐奖励(iPad、iPhone、MacBook Pro 等等)。伯乐推荐邮件格式:[伯乐推荐] 候选人姓名-职位名称-推荐人姓名-推荐人手机号。 「深度学习福利」大神带你进阶工程师,立即查看>>> 搭建过dg的同学肯定非常清楚归档日志和备份的重要性,这些归档日志从主库传输到备库,在备库被apply。 思考一个问题:主库的归档日志是怎么管理呢,备库的归档又该怎么管理呢? 在11g里面,随着ASM、RAC、Data Guard(包括Active Data Guard)的成熟,使用RAC+ASM+Data Guard越来越成为一种可靠的、维护简单、稳定的高可用性和容灾保护方案。这篇文章谈谈如何管理Oracle 11g Data Guard环境中的归档日志。 归档日志是重要的,不然就不必提到这篇文章, 备份恢复 需要它,而Data Guard也需要它。在早期版本的Data Guard环境中,常常面临着归档日志管理问题。在Data Guard环境里面,对归档日志管理需要达到以下几个方面的要求或者说是需求: 不能够随意删除掉归档日志,归档日志丢失会导致Data Guard需要重新搭建。 不能随意使用RMAN删除归档日志,否则同样会导致Data Guard需要重新搭建。 在使用RMAN备份后,如果归档没有被传送或应用到备库上,那么RMAN不应该删除归档日志,否则Data Guard需要的归档就必须从备份里面还原出来,增加了维护工作量。 对RMAN的备份脚本没有特别的要求,否则脚本意外改动,可能会导致Data Guard需要的归档日志被删除。 归档应尽量保存在磁盘上,以避免Data Guard长时间维护时归档被删除。 备库的归档日志不需要花精力去维护,自动删除已经应用过的归档日志。 幸运的是,在11g环境里面,上述的几点很容易就满足,那就是只需要做到以下几点。 使用快速恢复区(fast recovery area),在10g版本的文档中称为闪回恢复区(flash recovery area),老实说,一直不太明白为什么取名叫闪回恢复区,难道是因为10g有了数据库闪回功能?在RAC中,毫无疑问快速恢复区最好是置放在ASM上。 为快速恢复区指定合适的空间。首先我们需要预估一个合理的归档保留时间长。比如由于备份系统问题或Data Guard备库问题、维护等,需要归档保留的时间长度。假设是24小时,再评估一下在归档量最大的24小时之内,会有多少量的归档?一般来说是在批量数据处理的时候归档量最大,假设这24小时之内归档最大为200G。注意对于RAC来说是所有节点在这24小时的归档量之和。最后为快速恢复区指定需要的空间量,比通过参数db_recovery_file_dest_size指定快速恢复区的大小。这里同样假设快速恢复区们存放归档日志。 在备库上指定快速恢复区以及为快速恢复区指定合适的大小,在备库上指定快速恢复区的大小主要考虑的是:切换成为主库后归档日志容量;如果主库归档容量压力大,备库能否存储更多的归档日志以便可以通过备库来备份归档日志。 对主库和备份使用RMAN配置归档删除策略: CONFIGURE ARCHIVELOG DELETION POLICY TO APPLIED ON ALL STANDBY; 在主备库 需要指定归档路径为 快速恢复区,这样才好让rman来管理闪回区,log_archive_dest_size_1='location=USE_DB_RECOVERY_FILE_DEST' 完成了上述几个步骤,那么归档管理的要求基本上就达到了。通过这样的设置,实现的效果如下: 归档日志如果没有应用到备库,那么在RMAN中使用backup .... delete inputs all和delete archivelog all不会将归档日志删除。但但是请注意如果是使用delete force命令则会删除掉归档,不管归档有没有被应用到备库。 如果归档日志已经应用到了备库,那么在RMAN中使用backup .... delete inputs all和delete archivelog all可以删除归档日志,在正常情况下,由于归档日志可能很快应用到Data Guard,所以在RMAN备份之后可以正常删除归档日志。RMAN也不需要使用特别的备份脚本,也不必担心人为不小心使用。delete archivelog all命令删除了归档。 备库的归档日志存储到快速恢复区中,备库的快速恢复区空间紧张时,会自动删除已经应用过的较早的归档日志以释放空间,这样便可以实现备库的归档日志完全自动管理。 如果由于备份异常或Data Guard异常,在快速恢复区空间紧张时,Oracle在切换日志时,会自动删除掉已经应用过的归档日志,以释放空间。但是如果归档日志没有应用到Data Guard,那么归档日志不会被删除。这种情况下,快速恢复区的归档可能会增加到空间耗尽,最后就会出现数据库不能归档,数据库挂起的问题。 注意上面最后一点,当快速恢复区空间紧张时,Oracle开始删除归档日志,删除的条件还包括归档日志已经应用到备库,这种情况下如果归档日志还没有备份,也会被删除掉。这里的问题是,文档中描述的快速恢复区空间紧张,具体是指什么时间?也就是快速恢复区的空间消耗多少百分比的时候才算是空间紧张?在MOS文章《Files being deleted in the flash recovery area, messages in the alert log Deleted Oracle managed file (Doc ID 1369341.1)》里面有提到,空间使用率达到80%以后就开始删除文件(归档日志)。 Oracle在往快速恢复区存储文件时,其步骤大概是这样的:Oracle估计需要的空间大小(切换日志时就是归档日志大小),然后将这个大小与当前的占用空间大小相加,看是否超过了80%,如果超过了,那么就回收空间(回收的空间应大于等于新建文件需要的空间大小,也就是回收的空间以够用为原则)。如果不能回收空间(比如归档日志没有被应用到备库),那就只能继续占用新的空间,直到空间耗尽。 这里的问题是,假设快速恢复区设定了200G空间,那么在使用到80%,也就是160G的时候就开始回收空间。那么我们在估算空间时,就应该上浮20%。比如我们要求保留24小时归档,这24小时之内归档量最大是200G,那么我们应该为快速恢复区设置240G左右的容量。 那么,这个80%的比率能够更改吗以便延迟Oracle删除归档日志的时间吗?答案是肯定的。没有相应的数据库参数来设定,但是可以通过事件来设置,事件号是19823: oerr ora 19823 19823, 00000, "soft limit recovery area space pressure percentage" // *Document: NO // *Cause: Set on all instances to alter recovery area space pressure // trigger percentage. // *Action: level 1 to 100 indicates the percentage when the space // pressure has to be triggered. 下在是一个测试: 测试环境:主库是Oracle 11.2.0.3 for Linux两节点RAC,备库是Oracle 11.2.0.3 for linux单实例库。测试是在主库的节点1上进行的,其在线日志大小为512MB,快速恢复区指定的大小为16GB。 当前主库的FRA(快速恢复区)的使用率已经接近于80%: select * from V$RECOVERY_AREA_USAGE; FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES -------------------- ------------------ ------------------------- --------------- CONTROL FILE 0 0 0 REDO LOG 15.33 0 13 ARCHIVED LOG 64.04 63.81 45 BACKUP PIECE .24 0 1 IMAGE COPY 0 0 0 FLASHBACK LOG 0 0 0 FOREIGN ARCHIVED LOG 0 0 0 发现FRA(快速恢复区)的空间使用率基本上在80%左右。alert日志也有相应的删除较早的归档日志的信息: Thu Jan 02 12:28:50 2014 Thread 1 advanced to log sequence 981 (LGWR switch) Current log# 12 seq# 981 mem# 0: +DATA1/ractest/onlinelog/group_12.299.835542549 Current log# 12 seq# 981 mem# 1: +DG_FLA/ractest/onlinelog/group_12.298.835542551 Thu Jan 02 12:28:50 2014 LNS: Standby redo logfile selected for thread 1 sequence 981 for destination LOG_ARCHIVE_DEST_2 Thu Jan 02 12:28:50 2014 Deleted Oracle managed file +DG_FLA/ractest/archivelog/2014_01_02/thread_2_seq_309.424.835783855 Deleted Oracle managed file +DG_FLA/ractest/archivelog/2014_01_02/thread_1_seq_947.426.835783855 Deleted Oracle managed file +DG_FLA/ractest/archivelog/2014_01_02/thread_1_seq_948.437.835784237 Archived Log entry 2645 added for thread 1 sequence 980 ID 0xc8804744 dest 1: 上面的日志也可以看到其过程是:切换日志;删除不需要的最老的归档日志;生成新的归档日志。 现在我们利用事件19823将这个比率调到95%看看会是什么样子: SQL> alter system set event='19823 trace name context forever,level 95' scope=spfile sid='*'; 然后重启主库。再运行上面的测试代码,发现Oracle不再删除归档日志,而是到接近95%的空间使用率时再开始删除归档日志: FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES -------------------- ------------------ ------------------------- --------------- CONTROL FILE 0 0 0 REDO LOG 15.33 0 13 ARCHIVED LOG 68.99 65.72 49 BACKUP PIECE .24 0 1 IMAGE COPY 0 0 0 FLASHBACK LOG 0 0 0 FOREIGN ARCHIVED LOG 0 0 0 ............. FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES -------------------- ------------------ ------------------------- --------------- CONTROL FILE 0 0 0 REDO LOG 15.33 0 13 ARCHIVED LOG 78.62 59.9 55 BACKUP PIECE .24 0 1 IMAGE COPY 0 0 0 FLASHBACK LOG 0 0 0 FOREIGN ARCHIVED LOG 0 0 0 从上面的最后一次对v$recovery_area_usage的查询数据可以看到,此时空间利用率达到了94.19%,离95%已经很接近(在线日志的大小是512MB,占快速恢复区的3.1%,如果在快速恢复区里面多一个文件就会超过95%)。 接下来我们将这个比率调整到50%,看看是什么结果: SQL> alter system set event='19823 trace name context forever,level 50' scope=spfile sid='*'; 然后重启主库。再运行上面的测试代码,发现Oracle在删除归档日志,但是每次均删除的日志只需要容纳要新增的文件即可,不会一下子删除到使利用率到50%以下: FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES -------------------- ------------------ ------------------------- --------------- CONTROL FILE 0 0 0 REDO LOG 15.33 0 13 ARCHIVED LOG 72.47 48.57 54 BACKUP PIECE .24 0 1 IMAGE COPY 0 0 0 FLASHBACK LOG 0 0 0 FOREIGN ARCHIVED LOG 0 0 0 然后一直使用alter system switch logfile命令,每执行一次,Oracle会删除一个归档日志,到最后快速恢复区的空间利用率到接近于50%。 Thu Jan 02 12:56:29 2014 Thread 1 advanced to log sequence 1004 (LGWR switch) Current log# 12 seq# 1004 mem# 0: +DATA1/ractest/onlinelog/group_12.299.835542549 Current log# 12 seq# 1004 mem# 1: +DG_FLA/ractest/onlinelog/group_12.298.835542551 Thu Jan 02 12:56:30 2014 Deleted Oracle managed file +DG_FLA/ractest/archivelog/2014_01_02/thread_1_seq_963.317.835788195 Thu Jan 02 12:56:30 2014 LNS: Standby redo logfile selected for thread 1 sequence 1004 for destination LOG_ARCHIVE_DEST_2 Archived Log entry 2703 added for thread 1 sequence 1003 ID 0xc8804744 dest 1: FILE_TYPE PERCENT_SPACE_USED PERCENT_SPACE_RECLAIMABLE NUMBER_OF_FILES -------------------- ------------------ ------------------------- --------------- CONTROL FILE 0 0 0 REDO LOG 15.33 0 13 ARCHIVED LOG 33.29 28.86 65 BACKUP PIECE .24 0 1 IMAGE COPY 0 0 0 FLASHBACK LOG 0 0 0 FOREIGN ARCHIVED LOG 0 0 0 因此,我们可以了解event 19823的用途。对于空间容量比较小的主机,但是希望归档能够尽量保留在快速恢复区,以便留有足够的备份时间窗口,那么可以考虑把这个百分比调整到更大,比如90%,95%等。
「深度学习福利」大神带你进阶工程师,立即查看>>>
项目需要实现使用Kettle向神通数据库中写入数据,Kettle官方标准的数据库插件里面并没有对神通数据库的支持,因此需要自己写一个数据库插件。下面我们开始写一个数据库插件
1.在eclipse中创建一个maven项目,然后修改pom.xml的文件内容,最终内容如下
4.0.0 org.pentaho pentaho-ce-jar-parent-pom 8.1.0.0-365 kettle-plugins kettle-database-osrcar-plugin 0.0.1-SNAPSHOT jar kettle-database-osrcar-plugin http://maven.apache.org UTF-8 1.10.19 8.1.0.0-365 8.1.0.0-365 4.12 xerces xercesImpl 2.8.1 pentaho-kettle kettle-core ${pdi.version} provided junit junit ${junit.version} test maven-assembly-plugin distro-assembly package single false src/main/assembly/assembly.xml 2.在src/main下面创建一个assembly文件夹,然后在创建一个assembly.xml,文件内容如下:
bin zip ${project.build.directory} / *.jar lib/ false 此文件的作用是在target目录下生成一个zip格式的插件包,将插件包解压到kettle的plugins目下即可。项目若依赖到了其他jar包,会自动将依赖的jar包打包到lib目录下。
3.创建数据库插件类,在eclipse里面新建一个OscarDatabaseMeta,集成BaseDatabaseMeta,实现接口DatabaseInterface,并实现抽象类BaseDatabaseMeta的抽象方法,最终的类如下: package org.pentaho.di.core.database; import java.sql.ResultSet; import org.pentaho.di.core.Const; import org.pentaho.di.core.exception.KettleDatabaseException; import org.pentaho.di.core.plugins.DatabaseMetaPlugin; import org.pentaho.di.core.row.ValueMetaInterface; import org.pentaho.di.core.util.Utils; /** * DatabaseMeta数据库插件-神通数据库 */ @DatabaseMetaPlugin(type = "OSCAR", typeDescription = "神通数据库") public class OscarDatabaseMeta extends BaseDatabaseMeta implements DatabaseInterface { private static final String STRICT_BIGNUMBER_INTERPRETATION = "STRICT_NUMBER_38_INTERPRETATION"; @Override public int[] getAccessTypeList() { return new int[] { DatabaseMeta.TYPE_ACCESS_NATIVE, DatabaseMeta.TYPE_ACCESS_JNDI }; } @Override public int getDefaultDatabasePort() { if (getAccessType() == DatabaseMeta.TYPE_ACCESS_NATIVE) { return 2003; } return -1; } /** * 当前数据库是否支持自增类型的字段 */ @Override public boolean supportsAutoInc() { return false; } /** * 获取限制读取条数的数据,追加再select语句后实现限制返回的结果数 * @see org.pentaho.di.core.database.DatabaseInterface#getLimitClause(int) */ @Override public String getLimitClause(int nrRows) { return " WHERE ROWNUM <= " + nrRows; } /** * 返回获取表所有字段信息的语句 * @param tableName * @return The SQL to launch. */ @Override public String getSQLQueryFields(String tableName) { return "SELECT * FROM " + tableName + " WHERE 1=0"; } @Override public String getSQLTableExists(String tablename) { return getSQLQueryFields(tablename); } @Override public String getSQLColumnExists(String columnname, String tablename) { return getSQLQueryColumnFields(columnname, tablename); } public String getSQLQueryColumnFields(String columnname, String tableName) { return "SELECT " + columnname + " FROM " + tableName + " WHERE 1=0"; } @Override public boolean needsToLockAllTables() { return false; } @Override public String getDriverClass() { if (getAccessType() == DatabaseMeta.TYPE_ACCESS_ODBC) { return "sun.jdbc.odbc.JdbcOdbcDriver"; } else { return "com.oscar.Driver"; } } @Override public String getURL(String hostname, String port, String databaseName) throws KettleDatabaseException { if (getAccessType() == DatabaseMeta.TYPE_ACCESS_ODBC) { return "jdbc:odbc:" + databaseName; } else if (getAccessType() == DatabaseMeta.TYPE_ACCESS_NATIVE) { //
/ // :/ String _hostname = hostname; String _port = port; String _databaseName = databaseName; if (Utils.isEmpty(hostname)) { _hostname = "localhost"; } if (Utils.isEmpty(port) || port.equals("-1")) { _port = ""; } if (Utils.isEmpty(databaseName)) { throw new KettleDatabaseException("必须指定数据库名称"); } if (!databaseName.startsWith("/")) { _databaseName = "/" + databaseName; } return "jdbc:oscar://" + _hostname + (Utils.isEmpty(_port) ? "" : ":" + _port) + _databaseName; } else { throw new KettleDatabaseException("不支持的数据库连接方式[" + getAccessType() + "]"); } } /** * Oracle doesn't support options in the URL, we need to put these in a * Properties object at connection time... */ @Override public boolean supportsOptionsInURL() { return false; } /** * @return true if the database supports sequences */ @Override public boolean supportsSequences() { return true; } /** * Check if a sequence exists. * * @param sequenceName * The sequence to check * @return The SQL to get the name of the sequence back from the databases data * dictionary */ @Override public String getSQLSequenceExists(String sequenceName) { int dotPos = sequenceName.indexOf('.'); String sql = ""; if (dotPos == -1) { // if schema is not specified try to get sequence which belongs to current user sql = "SELECT * FROM USER_SEQUENCES WHERE SEQUENCE_NAME = '" + sequenceName.toUpperCase() + "'"; } else { String schemaName = sequenceName.substring(0, dotPos); String seqName = sequenceName.substring(dotPos + 1); sql = "SELECT * FROM ALL_SEQUENCES WHERE SEQUENCE_NAME = '" + seqName.toUpperCase() + "' AND SEQUENCE_OWNER = '" + schemaName.toUpperCase() + "'"; } return sql; } /** * Get the current value of a database sequence * * @param sequenceName * The sequence to check * @return The current value of a database sequence */ @Override public String getSQLCurrentSequenceValue(String sequenceName) { return "SELECT " + sequenceName + ".currval FROM DUAL"; } /** * Get the SQL to get the next value of a sequence. (Oracle only) * * @param sequenceName * The sequence name * @return the SQL to get the next value of a sequence. (Oracle only) */ @Override public String getSQLNextSequenceValue(String sequenceName) { return "SELECT " + sequenceName + ".nextval FROM dual"; } @Override public boolean supportsSequenceNoMaxValueOption() { return true; } /** * @return true if we need to supply the schema-name to getTables in order to * get a correct list of items. */ @Override public boolean useSchemaNameForTableList() { return true; } /** * @return true if the database supports synonyms */ @Override public boolean supportsSynonyms() { return true; } /** * Generates the SQL statement to add a column to the specified table * * @param tablename * The table to add * @param v * The column defined as a value * @param tk * the name of the technical key field * @param use_autoinc * whether or not this field uses auto increment * @param pk * the name of the primary key field * @param semicolon * whether or not to add a semi-colon behind the statement. * @return the SQL statement to add a column to the specified table */ @Override public String getAddColumnStatement(String tablename, ValueMetaInterface v, String tk, boolean use_autoinc, String pk, boolean semicolon) { return "ALTER TABLE " + tablename + " ADD " + getFieldDefinition(v, tk, pk, use_autoinc, true, false); } /** * Generates the SQL statement to drop a column from the specified table * * @param tablename * The table to add * @param v * The column defined as a value * @param tk * the name of the technical key field * @param use_autoinc * whether or not this field uses auto increment * @param pk * the name of the primary key field * @param semicolon * whether or not to add a semi-colon behind the statement. * @return the SQL statement to drop a column from the specified table */ @Override public String getDropColumnStatement(String tablename, ValueMetaInterface v, String tk, boolean use_autoinc, String pk, boolean semicolon) { return "ALTER TABLE " + tablename + " DROP COLUMN " + v.getName() + Const.CR; } /** * Generates the SQL statement to modify a column in the specified table * * @param tablename * The table to add * @param v * The column defined as a value * @param tk * the name of the technical key field * @param use_autoinc * whether or not this field uses auto increment * @param pk * the name of the primary key field * @param semicolon * whether or not to add a semi-colon behind the statement. * @return the SQL statement to modify a column in the specified table */ @Override public String getModifyColumnStatement(String tablename, ValueMetaInterface v, String tk, boolean use_autoinc, String pk, boolean semicolon) { ValueMetaInterface tmpColumn = v.clone(); String tmpName = v.getName(); boolean isQuoted = tmpName.startsWith("\"") && tmpName.endsWith("\""); if (isQuoted) { // remove the quotes first. // tmpName = tmpName.substring(1, tmpName.length() - 1); } int threeoh = tmpName.length() >= 30 ? 30 : tmpName.length(); tmpName = tmpName.substring(0, threeoh); tmpName += "_KTL"; // should always be shorter than 35 positions // put the quotes back if needed. // if (isQuoted) { tmpName = "\"" + tmpName + "\""; } tmpColumn.setName(tmpName); // Read to go. // String sql = ""; // Create a new tmp column sql += getAddColumnStatement(tablename, tmpColumn, tk, use_autoinc, pk, semicolon) + ";" + Const.CR; // copy the old data over to the tmp column sql += "UPDATE " + tablename + " SET " + tmpColumn.getName() + "=" + v.getName() + ";" + Const.CR; // drop the old column sql += getDropColumnStatement(tablename, v, tk, use_autoinc, pk, semicolon) + ";" + Const.CR; // create the wanted column sql += getAddColumnStatement(tablename, v, tk, use_autoinc, pk, semicolon) + ";" + Const.CR; // copy the data from the tmp column to the wanted column (again) // All this to avoid the rename clause as this is not supported on all Oracle // versions sql += "UPDATE " + tablename + " SET " + v.getName() + "=" + tmpColumn.getName() + ";" + Const.CR; // drop the temp column sql += getDropColumnStatement(tablename, tmpColumn, tk, use_autoinc, pk, semicolon); return sql; } @Override public String getFieldDefinition(ValueMetaInterface v, String tk, String pk, boolean use_autoinc, boolean add_fieldname, boolean add_cr) { StringBuilder retval = new StringBuilder(128); String fieldname = v.getName(); int length = v.getLength(); int precision = v.getPrecision(); if (add_fieldname) { retval.append(fieldname).append(" "); } int type = v.getType(); switch (type) { case ValueMetaInterface.TYPE_TIMESTAMP: case ValueMetaInterface.TYPE_DATE: retval.append("TIMESTAMP"); break; case ValueMetaInterface.TYPE_BOOLEAN: if (supportsBooleanDataType()) { retval.append("BOOLEAN"); } else { retval.append("CHAR(1)"); } break; case ValueMetaInterface.TYPE_NUMBER: case ValueMetaInterface.TYPE_INTEGER: case ValueMetaInterface.TYPE_BIGNUMBER: if (fieldname.equalsIgnoreCase(tk) || // Technical key fieldname.equalsIgnoreCase(pk) // Primary key ) { retval.append("BIGSERIAL"); } else { if (length > 0) { if (precision > 0 || length > 18) { // Numeric(Precision, Scale): Precision = total length; Scale = decimal places retval.append("NUMERIC(").append(length + precision).append(", ").append(precision).append(")"); } else if (precision == 0) { if (length > 9) { retval.append("BIGINT"); } else { if (length < 5) { retval.append("SMALLINT"); } else { retval.append("INT"); } } } else { retval.append("FLOAT(53)"); } } else { retval.append("DOUBLE PRECISION"); } } break; case ValueMetaInterface.TYPE_STRING: if (length < 1 || length >= DatabaseMeta.CLOB_LENGTH) { retval.append("TEXT"); } else { retval.append("VARCHAR(").append(length).append(")"); } break; case ValueMetaInterface.TYPE_BINARY: retval.append("BLOB"); break; default: retval.append(" UNKNOWN"); break; } if (add_cr) { retval.append(Const.CR); } return retval.toString(); } /* * (non-Javadoc) * * @see com.ibridge.kettle.core.database.DatabaseInterface#getReservedWords() */ @Override public String[] getReservedWords() { return new String[] { "ALIAS", "AND", "AS", "AT", "BEGIN", "BETWEEN", "BIGINT", "BIT", "BY", "BOOLEAN", "BOTH", "CALL", "CASE", "CAST", "CHAR", "CHARACTER", "COMMIT", "CONSTANT", "CURSOR", "COALESCE", "CONTINUE", "CONVERT", "CURRENT_DATE", "CURRENT_TIMESTAMP", "CURRENT_USER", "DATE", "DEC", "DECIMAL", "DECLARE", "DEFAULT", "DECODE", "DELETE", "ELSE", "ELSIF", "END", "EXCEPTION", "EXECUTE", "EXIT", "EXTRACT", "FALSE", "FETCH", "FLOAT", "FOR", "FROM", "FUNCTION", "GOTO", "IF", "IN", "INT", "INTO", "IS", "INTEGER", "IMMEDIATE", "INDEX", "INOUT", "INSERT", "LEADING", "LIKE", "LIMIT", "LOCALTIME", "LOCALTIMESTAMP", "LOOP", "NCHAR", "NEXT", "NOCOPY", "NOT", "NULLIF", "NULL", "NUMBER", "NUMERIC", "OPTION", "OF", "OR", "OUT", "OVERLAY", "PERFORM", "POSITION", "PRAGMA", "PROCEDURE", "QUERY", "RAISE", "RECORD", "RENAME", "RETURN", "REVERSE", "ROLLBACK", "REAL", "SELECT", "SAVEPOINT", "SETOF", "SMALLINT", "SUBSTRING", "SQL", "SYSDATE", "SESSION_USER", "THEN", "TO", "TYPE", "TABLE", "TIME", "TIMESTAMP", "TINYINT", "TRAILING", "TREAT", "TRIM", "TRUE", "TYPE", "UID", "UPDATE", "USER", "USING", "VARCHAR", "VARCHAR2", "VALUES", "WITH", "WHEN", "WHILE", "LEVEL" }; } /** * @return The SQL on this database to get a list of stored procedures. */ @Override public String getSQLListOfProcedures() { /* * return * "SELECT DISTINCT DECODE(package_name, NULL, '', package_name||'.') || object_name " * + "FROM user_arguments " + "ORDER BY 1"; */ return "SELECT name FROM ORM_FUNCTIONS union SELECT name FROM ORM_PROCEDURES"; } @Override public String getSQLLockTables(String[] tableNames) { StringBuilder sql = new StringBuilder(128); for (int i = 0; i < tableNames.length; i++) { sql.append("LOCK TABLE ").append(tableNames[i]).append(" IN EXCLUSIVE MODE;").append(Const.CR); } return sql.toString(); } @Override public String getSQLUnlockTables(String[] tableNames) { return null; // commit handles the unlocking! } /** * @return extra help text on the supported options on the selected database * platform. */ @Override public String getExtraOptionsHelpText() { return "http://www.shentongdata.com/?bid=3&eid=249"; } @Override public String[] getUsedLibraries() { return new String[] { "oscarJDBC.jar", "oscarJDBC14.jar", "oscarJDBC16.jar" }; } /** * Verifies on the specified database connection if an index exists on the * fields with the specified name. * * @param database * a connected database * @param schemaName * @param tableName * @param idx_fields * @return true if the index exists, false if it doesn't. * @throws KettleDatabaseException */ @Override public boolean checkIndexExists(Database database, String schemaName, String tableName, String[] idx_fields) throws KettleDatabaseException { String tablename = database.getDatabaseMeta().getQuotedSchemaTableCombination(schemaName, tableName); boolean[] exists = new boolean[idx_fields.length]; for (int i = 0; i < exists.length; i++) { exists[i] = false; } try { // // Get the info from the data dictionary... // String sql = "SELECT * FROM USER_IND_COLUMNS WHERE TABLE_NAME = '" + tableName + "'"; ResultSet res = null; try { res = database.openQuery(sql); if (res != null) { Object[] row = database.getRow(res); while (row != null) { String column = database.getReturnRowMeta().getString(row, "COLUMN_NAME", ""); int idx = Const.indexOfString(column, idx_fields); if (idx >= 0) { exists[idx] = true; } row = database.getRow(res); } } else { return false; } } finally { if (res != null) { database.closeQuery(res); } } // See if all the fields are indexed... boolean all = true; for (int i = 0; i < exists.length && all; i++) { if (!exists[i]) { all = false; } } return all; } catch (Exception e) { throw new KettleDatabaseException("Unable to determine if indexes exists on table [" + tablename + "]", e); } } @Override public boolean requiresCreateTablePrimaryKeyAppend() { return true; } /** * Most databases allow you to retrieve result metadata by preparing a SELECT * statement. * * @return true if the database supports retrieval of query metadata from a * prepared statement. False if the query needs to be executed first. */ @Override public boolean supportsPreparedStatementMetadataRetrieval() { return false; } /** * @return The maximum number of columns in a database, <=0 means: no known * limit */ @Override public int getMaxColumnsInIndex() { return 32; } /** * @return The SQL on this database to get a list of sequences. */ @Override public String getSQLListOfSequences() { return "SELECT SEQUENCE_NAME FROM all_sequences"; } /** * @param string * @return A string that is properly quoted for use in an Oracle SQL statement * (insert, update, delete, etc) */ @Override public String quoteSQLString(String string) { string = string.replaceAll("'", "''"); string = string.replaceAll("\\n", "'||chr(13)||'"); string = string.replaceAll("\\r", "'||chr(10)||'"); return "'" + string + "'"; } /** * Returns a false as Oracle does not allow for the releasing of savepoints. */ @Override public boolean releaseSavepoint() { return false; } @Override public boolean supportsErrorHandlingOnBatchUpdates() { return false; } /** * @return true if Kettle can create a repository on this type of database. */ @Override public boolean supportsRepository() { return true; } @Override public int getMaxVARCHARLength() { return 2000; } /** * Oracle does not support a construct like 'drop table if exists', which is * apparently legal syntax in many other RDBMSs. So we need to implement the * same behavior and avoid throwing 'table does not exist' exception. * * @param tableName * Name of the table to drop * @return 'drop table if exists'-like statement for Oracle */ @Override public String getDropTableIfExistsStatement(String tableName) { return "DROP TABLE IF EXISTS " + tableName; } @Override public SqlScriptParser createSqlScriptParser() { return new SqlScriptParser(false); } /** * @return true if using strict number(38) interpretation */ public boolean strictBigNumberInterpretation() { return "Y".equalsIgnoreCase(getAttributes().getProperty(STRICT_BIGNUMBER_INTERPRETATION, "N")); } /** * @param strictBigNumberInterpretation * true if use strict number(38) interpretation */ public void setStrictBigNumberInterpretation(boolean strictBigNumberInterpretation) { getAttributes().setProperty(STRICT_BIGNUMBER_INTERPRETATION, strictBigNumberInterpretation ? "Y" : "N"); } } 这是一个完整的代码,这个类里面不仅实现父类的抽象方法,还重写的一部分。这个类只需要getFieldDefinition、getDriverClass、getURL、getAddColumnStatement、getModifyColumnStatement、getUsedLibraries、getAccessTypeList实现这些方法就可以编译通过。其他的方法要根据实际的数据库来决定是否需要实现,抽象类里面已经实现了一部分,可以到BaseDatabaseMeta中看一其实现的方式能否满足自己使用数据库的需求,如果不满足则需要重写。方法getFieldDefinition()的实现尤其重要,这个是实现数据类型转换所需要,编写不好会导致插入数据的时候出现问题。 4.在pom.xml右键执行打包,会在target目录下生成一个jar包和一个zip包,由于本插件没有其他依赖,直接将jar包放到plugins目录下或者自己再创建一个文件夹。依赖其他jar包时,可以将zip解压到plugins目录下 5.重新启动kettle,新建DB连接,就可以看到里面已经有神通数据库了。 附件: 点击下载插件源代码 「深度学习福利」大神带你进阶工程师,立即查看>>>
写了两个kettle插件,这两个插件是两个插件项目,且在两个分类,开发完成打包到放到kettle的plugins目录下,启动kettle,结果在kettle设计器中只显示了一个插件。后来改了插件名称,插件在plugins文件夹中的文件夹的名字,发现始终是先扫描到谁就可以注册进来,后续的不管有几个都进不来,后果跟了一下kettle的源代码,发现是kettle里面存储插件TreeSet的Comparator写的有问题。
负责注册插件的类org.pentaho.di.core.plugins.PluginRegistry中声明了一个Map存放所有的插件类型及插件。key是插件类型如作业项插件、步骤插件类型等,value是一个TreeSet,TreeSet中存储的是改插件类型所有的插件。(Kettle8.1.0.0-365版本在PluginRegistry的91行) private final Map
, Set> categoryMap = new HashMap<>(); 下面看一下注册插件的方法 public void registerPlugin( Class extends PluginTypeInterface> pluginType, PluginInterface plugin ) throws KettlePluginException { boolean changed = false; // Is this an add or an update? lock.writeLock().lock(); try { if ( plugin.getIds()[0] == null ) { throw new KettlePluginException( "Not a valid id specified in plugin :" + plugin ); } // Keep the list of plugins sorted by name... // Set list = pluginMap.computeIfAbsent( pluginType, k -> new TreeSet<>( Plugin.nullStringComparator ) ); if ( !list.add( plugin ) ) { list.remove( plugin ); list.add( plugin ); changed = true; } if ( !Utils.isEmpty( plugin.getCategory() ) ) { // Keep categories sorted in the natural order here too! // categoryMap.computeIfAbsent( pluginType, k -> new TreeSet<>(getNaturalCategoriesOrderComparator(pluginType ))) .add( plugin.getCategory() ); } } finally { lock.writeLock().unlock(); Set listeners = this.listeners.get( pluginType ); if ( listeners != null ) { for ( PluginTypeListener listener : listeners ) { // Changed or added? if ( changed ) { listener.pluginChanged( plugin ); } else { listener.pluginAdded( plugin ); } } } synchronized ( this ) { notifyAll(); } } } 重点看方法中的这一行代码: categoryMap.computeIfAbsent( pluginType, k -> new TreeSet<>(getNaturalCategoriesOrderComparator(pluginType ))) .add( plugin.getCategory() ); 这一行是当map中不存在指定的ke时,返回一个空的TreeSet.关键是TreeSet指定了一个Comparator。 我们接下来看一下getNaturalCategoriesOrderComparator这个方法是怎么创建Comparator的。这个方法的目的是对Kettle自身的插件进行排序按顺序显示。 private static Comparator getNaturalCategoriesOrderComparator( Class extends PluginTypeInterface> pluginType ) { PluginTypeCategoriesOrder naturalOrderAnnotation = pluginType.getAnnotation( PluginTypeCategoriesOrder.class ); final String[] naturalOrder; if ( naturalOrderAnnotation != null ) { String[] naturalOrderKeys = naturalOrderAnnotation.getNaturalCategoriesOrder(); Class> i18nClass = naturalOrderAnnotation.i18nPackageClass(); naturalOrder = Arrays.stream( naturalOrderKeys ) .map( key -> BaseMessages.getString( i18nClass, key ) ) .toArray( String[]::new ); } else { naturalOrder = null; } return ( s1, s2 ) -> { if ( naturalOrder != null ) { int idx1 = Const.indexOfString( s1, naturalOrder ); int idx2 = Const.indexOfString( s2, naturalOrder ); return idx1 - idx2; } return 0; }; } 按照这个方式的实现,自定了两个插件且插件在两个分类中且分类不是Kettle原有的分类,那么idx1和idx2的值均是-1,如此compare就会返回0,0表示连个分类是相同的就添加不到set中,所以第二个及以后的插件都注册不进来。修改的代码如下: private static Comparator getNaturalCategoriesOrderComparator( Class extends PluginTypeInterface> pluginType ) { PluginTypeCategoriesOrder naturalOrderAnnotation = pluginType.getAnnotation( PluginTypeCategoriesOrder.class ); ........ return ( s1, s2 ) -> { if ( naturalOrder != null ) { int idx1 = Const.indexOfString( s1, naturalOrder ); int idx2 = Const.indexOfString( s2, naturalOrder ); // 两个插件分类都不是Kettle自己已有的分类是,则让两个分类进行进行比较 if (-1 == idx1 && -1 == idx2) return s1.compareTo(s2); // 自定义的插件显示在末尾 if (-1 == idx1 || -1 == idx2) return 1; return idx1 - idx2; } return 0; }; } 「深度学习福利」大神带你进阶工程师,立即查看>>> 第一步:准备低成本存储的业务数据和DLA表 OSS(https://www.aliyun.com/product/oss)是云上低成本数据存储的优选方案 DLA(https://www.aliyun.com/product/datalakeanalytics)是云上低成本、无服务器化的支持OSS数据查询、分析的优选方案 参考如下文档使用案例,准备OSS上存储的业务数据和DLA表: https://yq.aliyun.com/articles/623282 此步骤预计耗时:5分钟。 第二步:使用DataV访问DLA制作数据大屏 DataV(https://data.aliyun.com/visual/datav)是云上大数据可视化大屏的优选方案 1. 准备DataV 以第一步中的业务数据为例,构建企业销售数据大屏,本大屏主要涉及三张表: orders表,销售订单数据; customer表,客户记录数据; nation表,国家记录数据; 登录控制台DataV控制台:http://datav.aliyun.com/data,购买基础版: 使用“兼容MySQL”的方式,就能连接DLA服务,本例中基础版就能满足。 2. 准备DLA数据源 点击 “我的数据”,“添加数据” 编辑数据源: 选择“兼容MySQL数据库”类型; 名称按需进行命名; 根据在DLA控制台https://datalakeanalytics.console.aliyun.com/overview上的链接信息(经典网络)和在阿里云站内信收到用户名、密码信息,填入相应栏目,选择目标的tpch数据库,确定保存。 3. 准备大屏模板 点击 “我的可视化”,“新建可视化” 选择“销售实时监控模板”,点“创建” 本示例大屏中,目标显示各个国家的销售数据情况,把下面标红的框内的组件删除。 然后为了布局,美观,重新调整一个组件在画布上的位置。 为了显示各个国家的销售数据情况,需要世界地图,删除现有的中国地图组件,然后在导航栏的“地图”中选择“3D平面世界地图”。 4. 给大屏中的组件配置数据 4.1 给地图配置数据 根据如下操作,给地图render数据: 选择地图,在数据tab页面中,“数据源类型数据库选择已有数据源我的数据**”中配置的DLA数据源; 在SQL中,填入如下SQL,计算按国家销售额排序的数据; 字段id和value,分别填入上述SQL返回的n_id列和total_price列; 根据业务数据更新需求(见附一:架构示意),选择大屏数据“自动更新请求”,比如60秒一次; 然后点击“刷新数据”。 4.2 给总销售额配置数据 根据如下操作,给总销售额配置数据: 选择销售总额组件,在数据tab页面中,“数据源类型数据库选择已有数据源我的数据**”中配置的DLA数据源; 在SQL中,填入如下SQL,计算销售总额的数据; select sum(o_totalprice) total_price from orders; 字段value,填入上述SQL返回的total_price列; 根据业务数据更新需求(见附一:架构示意),选择大屏数据“自动更新请求”,比如60秒一次; 然后点击“刷新数据”。 4.3 给按国家销售排名配置数据 根据如下操作,给总销售额配置数据: 选择销售额国家排名组件,在数据tab页面中,“数据源类型数据库选择已有数据源我的数据**”中配置的DLA数据源; 在SQL中,填入如下SQL,计算按国家销售额排序的数据; 字段value和content,填入上述SQL返回的total_price列和n_name列; 根据业务数据更新需求(见附一:架构示意),选择大屏数据“自动更新请求”,比如60秒一次; 然后点击“刷新数据”。 5. 预览和发布大屏 点击右上角的“预览”,可以看到大屏发布后的效果。 确认无误后,可以进行发布: 此步骤预计耗时:10分钟。 附:架构示意 和使用传统数据库加DataV构建大屏相比,DataV + DLA + OSS的方案是另外一种低成本选择,在绝大部分频率较低的数据刷新的业务大屏场景下,DataV + DLA + OSS的方式远比DataV + 传统数据库的成本低。结合业务数据的产生,整体架构示意如下: 可能的业务数据产出的大屏数据刷新链路: 业务应用程序产出的增量数据,直接上传到OSS中,通过定时查询,刷新到业务数据大屏中; 业务应用程序产出的业务日志,采集到日志服务中,通过定时日志投递(最小5分钟延迟),投递到OSS中,再通过定时查询,刷新到业务数据大屏中。 原文链接
「深度学习福利」大神带你进阶工程师,立即查看>>> 在阿里云上,很多客户的应用都是多地域部署的, 比如在北京(cn-beijing)的地域部署一个应用让北方的客户访问快一点,同时在杭州(cn-hangzhou)地域部署一份让南方的客户访问快一点。多地域部署之后,业务数据被拆成了多份,而各个地域的数据库都是独立的,网络又不通,给总体业务数据的分析造成了困难。今天我给大家介绍一套基于 DataLakeAnalytics , OSS , DataX 等几个阿里云产品的跨地域数据分析的解决方案。 其实云产品本身(比如我们 DataLakeAnalytics 自己)也有跨地域数据分析的需求,这个方案也同样适用。这个方案本来就是为了分析 DataLakeAnalytics 自己的业务数据而探索出来的。 方案概览 我们知道各个地域的RDS是不通的,除非你开公网访问权限(有很大的安全风险,不推荐), 而且即使你开公网,要对多个数据库里面的数据进行联合分析也不是一件容易的事情;而且这种数据分析的需求我们不希望它占用太多的预算。 我们的方案是把各个地域的数据都同步到同一个地域的OSS上面去,然后用 DataLakeAnalytics 进行联合分析。这个方案的优点在于 OSS 存储收费非常便宜, DataLakeAnalytics 也是按查询量收费的,你平时不查询的时候一分钱都不用花。总体方案如下图: 汇聚各个地域的数据 我们方案的第一步是把各个地域的RDS数据同步到同一个地域的OSS里面去。阿里巴巴集团开源了一个很棒的数据搬运的工具: DataX , 可以把数据在各种不同的数据源之间进行搬运,它支持的数据源类型非常丰富: 从关系型的 MySQL, SQLServer, 到各种文件系统如 HDFS, OSS等等,其中我们需要的是从 MySQL 读数据的 mysqlreader 插件以及往 OSS 写数据的 osswriter 插件。 假定我们有下面这么一个记录人员信息的表 person 需要同步: create table person ( id int primary key auto_increment, name varchar(1023), age int ); 我们写一个类似下面这样的DataX任务描述文件 person.json : { "job": { "setting": { "speed": { "channel": 1, "byte": 104857600 }, "errorLimit": { "record": 10 } }, "content": [ { "reader": { "name": "mysqlreader", "parameter": { "username": "your-user-name", "password": "your-password", "column": [ "id", "name", "age", ], "connection": [ { "table": [ "person" ], "jdbcUrl": [ "jdbc:mysql://your-rds.mysql.rds.aliyuncs.com:3306/dbname" ] } ] } }, "writer": { "name": "osswriter", "parameter": { "endpoint": "http://oss.aliyuncs.com", "accessId": "your-access-id", "accessKey": "your-access-secret", "bucket": "mydb-bucket", "object": "mydb/person/region=cn-hangzhou/person.csv", "encoding": "UTF-8", "fieldDelimiter": "|", "writeMode": "truncate" } } } ] } } 这里 MySQL 相关的信息填你的业务库的信息,而 OSS 相关的信息选择一个我们同步到的OSS的地址。注意 OSS 配置部分的 object 字段, mydb 保存你所有的数据, person 这个目录保存你的 person 表的数据, region=cn-hangzhou 这个目录就有意思了,它保存的是你的应用在 cn-hangzhou 这个region里面的数据,同样的,你可能还会有 cn-beijing , cn-shangahi 的数据等等。 然后执行如下命令: // 执行前确保你已经下载并正确配置好 DataX 了。 python datax/bin/datax.py person.json 正确执行的话你会看到下面的输出: .....省略N行...... 2018-09-06 19:53:19.900 [job-0] INFO JobContainer - PerfTrace not enable! 2018-09-06 19:53:19.901 [job-0] INFO StandAloneJobContainerCommunicator - Total 251 records, 54067 bytes | Speed 5.28KB/s, 25 records/s | Error 0 records, 0 bytes | All Task WaitWriterTime 0.001s | All Task WaitReaderTime 0.026s | Percentage 100.00% 2018-09-06 19:53:19.902 [job-0] INFO JobContainer - 任务启动时刻 : 2018-09-06 19:53:09 任务结束时刻 : 2018-09-06 19:53:19 任务总计耗时 : 10s 任务平均流量 : 5.28KB/s 记录写入速度 : 25rec/s 读出记录总数 : 251 读写失败总数 : 0 这样数据就自动同步到 OSS 上去了,你可以下载一个 oss-browser 去查看oss上面的数据: 文件里面数据大概是这样的: 9|ethan|10 10|julian|20 11|train|30 12|wally|40 完成了一个地域的数据搬运之后,其它地域都可以照葫芦画瓢,唯一需要注意的地方是,虽然 MySQL 数据是各个 地域 的数据,但是 OSS 要用同一个根目录 person ,因为我们要做数据汇集嘛,把几个地域的数据汇集完成之后, person 目录的结构大概是这样的: 使用 DataLakeAnalytics 分析汇聚后的OSS数据 下面的分析就可以交给 DataLakeAnalytics 了,分析OSS上的数据是 DataLakeAnalytics 的拿手好戏,在开始之前我们要有一个 DataLakeAnalytics 的账号,目前 DataLakeAnalytics 正在公测,直接申请试用就好了。试用审批成功之后,你会获得一个用户名和密码, 然后在控制台登录就可以使用: 或者如果你是极客,更偏爱命令行,你也可以使用普通的 MySQL 客户端就可以连接 DLA 了: mysql -hservice.cn-shanghai.datalakeanalytics.aliyuncs.com -P10000 -u -p 在这篇文章里面,我会使用 MySQL 命令行给大家演示 DLA 的功能。 首先我们来建一个 DataLakeAnalytics 的数据库: CREATE DATABASE `mydb` WITH DBPROPERTIES ( catalog = oss, location = 'oss://your-bucket/mydb/' ); 这里的 oss://mydb-bucket/mydb/ 就是前面我们数据汇聚的 person 目录的父目录。 建好库之后,我们再建一个表: CREATE EXTERNAL TABLE IF NOT EXISTS `person` ( `id` bigint, `name` varchar(128), `age` int ) PARTITIONED BY (region varchar(63)) ROW FORMAT DELIMITED FIELDS TERMINATED BY '|' STORED AS TEXTFILE LOCATION 'oss://mydb-bucket/mydb/person'; 注意这是一个分区表,分区的key是我们的region,这样的好处一是各个地域在同步数据的时候比较简单,不用担心把别的地域的数据冲掉了;另外利用地域分区也使得我们在分析单个地域的时候扫描数据量会比较小,查询速度更快。 建好表之后,我们运行如下命令让 DataLakeAnalytics 去对OSS上的文件列表进行扫描以找到所有的 region 分区: mysql> msck repair table person; +-----------------------------------------------------------------------------------------------------------+ | Result | +-----------------------------------------------------------------------------------------------------------+ | Partitions not in metastore: person:region=cn-beijing person:region=cn-hangzhou person:region=cn-shanghai | | Repair: Added partition to metastore mydb.person:region=cn-beijing | | Repair: Added partition to metastore mydb.person:region=cn-hangzhou | | Repair: Added partition to metastore mydb.person:region=cn-shanghai | +-----------------------------------------------------------------------------------------------------------+ 现在我们就可以开心的对所有地域的数据进行联合查询了 :) mysql> select * from person limit 5; +------+-------+------+-------------+ | id | name | age | region | +------+-------+------+-------------+ | 1 | james | 10 | cn-beijing | | 2 | bond | 20 | cn-beijing | | 3 | lucy | 30 | cn-beijing | | 4 | lily | 40 | cn-beijing | | 5 | trump | 10 | cn-hangzhou | +------+-------+------+-------------+ 5 rows in set (0.43 sec) mysql> select region, count(*) cnt from person group by region; +-------------+------+ | region | cnt | +-------------+------+ | cn-beijing | 4 | | cn-hangzhou | 4 | | cn-shanghai | 4 | +-------------+------+ 3 rows in set (0.18 sec) 总结 在这篇文章里面,我们介绍了一种通过 DataLakeAnalytics , OSS, DataX 进行跨地域数据分析的方法。限于篇幅的原因方案的很多细节没有进一步优化,比如我们其实可以对数据进行进一步按天分区,这样每天同步的数据可以更少,效率更高;再比如我们没有介绍如何周期性的进行数据同步,用crontab? 还是什么调度系统?这些就留给读者自己去探索了。 原文链接
「深度学习福利」大神带你进阶工程师,立即查看>>> 一 总体描述 自己的实验虚拟数据库中的 alert 不停的报下面的错误.alert日志刷的闹心. Sun Jun 10 23:44:42 2012 Error s in file /u01/app/ oracle /product/10.2.0/db_1/admin/testb/bdump/testb_j000_4944.trc: ORA-00604: error occurred at recursive SQL level 1 ORA-08102: index key not found, obj# 239, file 1, block 1674 (2) ORA-12012: error on auto execute of job 21 ORA-08102: index key not found, obj# 239, file 1, block 1674 (2) 二 操作环境 OS $cat / etc /redhat-release Red Hat Enterprise Linux Server release 5.6 (Tikanga) $ uname -a Linux stu00 2.6.18-238.el5 #1 SMP Tue Jan 4 15:24:05 EST 2011 i686 i686 i386 GNU/Linux DB SQL> set lines 150 COL PRODUCT FORMAT A55 COL VERSION FORMAT A15 COL STATUS FORMAT A15 SELECT * FROM PRODUCT_COMPONENT_VERSION;SQL> SQL> SQL> SQL> PRODUCT VERSION STATUS NLSRTL 10.2.0.4.0 Production Oracle Database 10g Enterprise Edition 10.2.0.4.0 Prod PL/SQL 10.2.0.4.0 Production TNS for Linux : 10.2.0.4.0 Production SQL> other 三 排错思路概况 1.报错信息ORA-12012: error on auto execute of job 21,ORA-08102: index key not found, obj# 239, file 1, block 1674 (2) 是由于job 21执行的时候没有找到索引key.所以alert日志不停的刷. 2.查询job 3.查询索引 4.重建索引 四 详细步骤操作 1.查看 /u01/app/ oracle /product/10.2.0/db_1/admin/testb/bdump/testb_j000_4944.trc文件内容(汗,看不懂,这里跳过) 2.根据排错思路查看job 21的情况: SQL> r 1* select job,log_user,interval,what from dba_jobs where job=21 JOB LOG_USER INTERVAL WHAT 21 SYS sysdate + 1 / (24 * 60) EMD_MAINTENANCE. EXECUTE _EM_DBMS_JOB_PROCS; SQL> 原来是EM的维护操作,1分钟执行一次. 3.查询obj# 239号是什么obj: SQL> r 1* select owner,object_name,object_id,object_type,status from DBA_OBJECTS where OBJECT_ID=239 OWNER OBJECT_NAM OBJECT_ID OBJECT_TYPE STATUS SYS I_JOB_NEXT 239 INDEX VALID SQL> obj# 239真是一个索引哈,看status是valid的,这里竟然不一致.不管为什么不一致了,先解决这个alert再说. (嘿嘿,实验机有快照,有兴趣再研究的请留言) 4.重建问题索引:(使用在线重建方式) SQL> ALTER INDEX I_JOB_NEXT REBUILD ONLINE; Index altered. SQL> 5.观察alert日志是否还有错误产生. 6.日志已无报错. 五 个人总结 面对报错不要心急,静心的去分析每一个报错提示.官方支持文档中有说使用索引重建方式依然解决不了 alert 日志报错的.唯有重建索引一种方式.这里附上使用dbms_metadata.get_ddl查找索引ddl的方式. SQL> select DBMS_METADATA.GET_DDL('INDEX','I_JOB_NEXT','SYS') from dual; DBMS_METADATA.GET_DDL('INDEX','I_JOB_NEXT','SYS') CREATE INDEX "SYS"."I_JOB_NEXT" ON " SYS "."JOB$" ("NEXT_DATE") PCTFREE 10 I SQL > 六 资料参考引用 ORA-08102: TRYING TO MANIPULATE A JOB IN DBA_JOBS [ID 1036858.6]
「深度学习福利」大神带你进阶工程师,立即查看>>> 三者之间的区别(mysql 5.6+) 存储范围 占用空间(字节) 是否与时区相关 默认值 存储方式 编码方式 是否根据时区转换 date
1000-01-01 至 9999-12-31 3 无关 null 二进制 1bit用来存符号位,表示正负 1 bit sign (1= non-negative, 0= negative) 17bit用来存放年和月 17 bits year*13+month (year 0-9999, month 0-12) 5bit用来存放日 5 bits day (0-31) --------------------------- 一共23bit约为3个字节 23 bits = 3 bytes 否 datetime 1000-01-01 00:00:00 至 9999-12-31 23:59:59 5 当保存毫秒部分时使用额外的空间 (1到3 字节) 无关 null 二进制 1bit用来存符号位,表示正负 1 bit sign (1= non-negative, 0= negative) 17bit用来存放年和月 17 bits year*13+month (year 0-9999, month 0-12) 5bit用来存放日 5 bits day (0-31) 5bit用来存放时 5 bits hour (0-23) 5bit用来存放分 6 bits minute (0-59) 5bit用来存放秒 6 bits second (0-59) --------------------------- 一共40bit刚好5个字节 40 bits = 5 bytes 否 timestamp 1970-01-01 00:00:00 至 2038-01-09 03:14:07 4 当保存毫秒部分时使用额外的空间 (1到3 字节) 相关 CURRENT_TIMESTAMP(当前时间) 默认情况下, insert、update 数据时,TIMESTAMP列会自动以当前时间填充/更新 二进制 存的是自【1970-01-01,UTC】以来的秒数。 是 PS:在5.6版本之后,datetime开始可以使用current time作为默认值,所以不再有timestamp比datetime更适合作为需要随插入和更新来变更时间的类型的说法 date这个主要是表示日期的,用途上面没有什么需要抉择的。 对于timestamp和datetime,道友们可能还有选择困难症,在这里我发表一下自己的看法: 在5.6+的mysql数据库中,timestamp这个基本可以弃用了,以datetime为主。如果遇到跨时区的需求时,建议使用long或者bigint进行时间戳的存储,然后在代码中进行转换,不建议用timestamp; Mysql相关文档:https://dev.mysql.com/doc/internals/en/date-and-time-data-type-representation.html 参考博文:https://juejin.im/entry/5b6fcb1b6fb9a009d15a2c5c 「深度学习福利」大神带你进阶工程师,立即查看>>> 1.安装memcache: http://www.runoob.com/memcached/window-install-memcached.html 2、下载php_memcache扩展 扩展php7下载地址: https://github.com/nono303/PHP7-memcache-dl http://pecl.php.net/package/memcache/3.0.8/windows 或 http://windows.php.net/downloads/pecl/releases/memcache/3.0.8/ 3、把php_memcached.dll扔进 php安装路径下的ext文件夹 4、修改php.ini 加一行(若存在直接#注释) extension=php_memcache.dll 5、保存,重启apache,也就是重启(phpstudy或xamp或....) 6.然后 在php页面输出phpinfo();检查 memcache 是否成功加载了。 本章节重点难点(安装memcache时一定要用管理员身份安装哦,下载php_memcache扩展时注意选 TS/NTS 和x86/x64版本,可在phpinfo查到) 如果成功加载了 ,就可以 在一个php页面做 memcache测试了 public function testMemcache() { $memcache = new Memcache; $memcache->connect('127.0.0.1', 11211) or die('shit'); $memcache->set('key', 'hello memcache!'); $out = $memcache->get('key'); echo $out; } 成功的话会输出 hello memcache!
「深度学习福利」大神带你进阶工程师,立即查看>>> 一、解压后配置 1,解压Redis-x64-3.2.100.zip,解压后的文件结构如下: 2,修改redis.windows.conf文件,设置maxmemory 大小 设置redis密码 redis启动命令: D:\Redis-x64-3.2.100> redis-server redis.windows.conf 启动成功 就可以访问redis了 D:\Redis-x64-3.2.100> .\redis-cli.exe 二、设置Redis服务 1、由于上面虽然启动了redis,但是只要一关闭cmd窗口,redis就会消失。所以要把redis设置成windows下的服务。 也就是设置到这里,首先发现是没用这个Redis服务的。 2、设置服务命令 redis-server --service-install redis.windows-service.conf --loglevel verbose 输入命令之后没有报错,表示成功了,刷新服务,会看到多了一个redis服务。 3、常用的redis服务命令。 卸载服务:redis-server --service-uninstall 开启服务:redis-server --service-start 停止服务:redis-server --service-stop 4、启动服务 5、测试Redis 安装测试成功。 这里只是做简单的安装,部署服务使用,更深入的使用可以去redis中文网看看 https://www.redis.net.cn/博客园也有很多深入使用方法.在下载的解压包里面有一份文档,有详细的说明, 3.可视化窗口: 下载地址: https://redisdesktop.com/download
「深度学习福利」大神带你进阶工程师,立即查看>>> linux安装参考: https://blog.csdn.net/gaokcl/article/details/83109684 一,Windows安装: 1,下载地址: https://github.com/MicrosoftArchive/redis/releases/tag/win-3.2.100 ( redis官网: https://redis.io/ ) 2,安装Redis-x64-3.2.100.msi ( 安装步骤,下一步下一步,有选择框选中即可), https://www.cnblogs.com/juncaoit/p/10122642.html 安装完成配置 3,设置 redis密码 : requirepass 123456 (我的是 123456) Ctrl+S 4,右击计算机(我的电脑) ===》管理 window + R 输入 cmd ===> cd D:\Redis-x64-3.2.100 ===> redis-cli =====> auth 123456
「深度学习福利」大神带你进阶工程师,立即查看>>> 事情是这样的。 这样的代码能行: $servername = "localhost"; $username = "username"; $password = "password"; // 创建连接 $conn = new mysqli($servername, $username, $password); // 检测连接 if ($conn->connect_error) { die("连接失败: " . $conn->connect_error); } echo "连接成功"; 这样的代码能行: $servername = "localhost"; $username = "username"; $password = "password"; $link = mysqli_connect($servername, $username, $password, 'wxjs'); if (!$link) { echo "Error: Unable to connect to MySQL." . PHP_EOL; echo "Debugging errno: " . mysqli_connect_errno() . PHP_EOL; echo "Debugging error: " . mysqli_connect_error() . PHP_EOL; exit; } echo "Success: A proper connection to MySQL was made! The my_db database is great." . PHP_EOL; echo "Host information: " . mysqli_get_host_info($link) . PHP_EOL; mysqli_close($link); 输出: Success: A proper connection to MySQL was made! The my_db database is great. Host information: Localhost via UNIX socket 这样的代码不行: $servername = "127.0.0.1"; $username = "username"; $password = "password"; $link = mysqli_connect($servername, $username, $password, 'wxjs'); if (!$link) { echo "Error: Unable to connect to MySQL." . PHP_EOL; echo "Debugging errno: " . mysqli_connect_errno() . PHP_EOL; echo "Debugging error: " . mysqli_connect_error() . PHP_EOL; exit; } echo "Success: A proper connection to MySQL was made! The my_db database is great." . PHP_EOL; echo "Host information: " . mysqli_get_host_info($link) . PHP_EOL; mysqli_close($link); 输出: Error: Unable to connect to MySQL. Debugging errno: 2002 Debugging error: Permission denied 得出结论:用IP和用localhost是不一样的。 我还找到了一个很变态的方法解决 用 IP 不行的,居然是关掉防火墙…… $ sudo setenforce 0 MySQL 新手。幼稚了……
「深度学习福利」大神带你进阶工程师,立即查看>>> 【mysql】mysql5.7&CentOS6.8二进制方式安装 2018年05月18日 11:32:18 debimeng 阅读数:2726 版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/debimeng/article/details/80362087 mysql5.7&CentOS6.8二进制方式安装 --环境描述 系统: CentOS6.8_x86_64 mysql: mysqlmysql-5.7.21 安装包:mysql-5.7.21-linux-glibc2.12-x86_64.tar.gz #注二进制安装包 安装包地址: https://dev.mysql.com/downloads/mysql/5.7.html#downloads 注:二进制包的话Select Operating System:这里勾选Linux-Generic --将mysql二进制包上传到服务器并解压 先创建/data,然后将安装包上传到这里 # mkdir /data 解压安装包到/data目录 # tar zxvf mysql-5.7.21-linux-glibc2.12-x86_64.tar.gz -C /data #注意如果需要解压到指定的目录需要加-C参数 更改文件夹名 # mv /data/mysql-5.7.21-linux-glibc2.12-x86_64 /data/mysql-5.7.21 --安装依赖包,使用yum安装,故前提条件是网络源的yum方式可用 # yum -y install libaio --创建mysql用户和组 # groupadd mysql # useradd -g mysql mysql --配置数据库目录 数据目录: /data/mysql-5.7.21/data 参数文件my.cnf: /data/mysql-5.7.21/etc/my.cnf 错误日志log-error: /data/mysql-5.7.21/log/mysql_error.log 二进制日志log-bin: /data/mysql-5.7.21/log/mysql_bin.log 慢查询日志slow_query_log_file: /data/mysql-5.7.21/log/mysql_slow_query.log 套接字socket文件: /data/mysql-5.7.21/run/mysql.sock pid文件: /data/mysql-5.7.21/run/mysql.pid 注:其中的mysql-5.7.21尽量和安装的mysql版本号一致,便宜管理。 # mkdir -p /data/mysql-5.7.21/{data,log,etc,run} # touch /data/mysql-5.7.21/log/{mysql_error.log,mysql_bin.log,mysql_slow_query.log} # chown -R mysql:mysql /data/mysql-5.7.21 # chmod 750 /data/mysql-5.7.21/{data,log,etc,run} --配置环境变量 # echo "export PATH=$PATH:/data/mysql-5.7.21/bin" >> /etc/profile --配置my.cnf文件 # touch /data/mysql-5.7.21/etc/my.cnf # chown mysql:mysql /data/mysql-5.7.21/etc/my.cnf my.cnf文件内容如下: [client] port = 3306 socket = /data/mysql-5.7.21/run/mysql.sock [mysqld] port = 3306 socket = /data/mysql-5.7.21/run/mysql.sock pid_file = /data/mysql-5.7.21/run/mysql.pid datadir = /data/mysql-5.7.21/data default_storage_engine = InnoDB max_allowed_packet = 128M max_connections = 2048 open_files_limit = 65535 skip-name-resolve lower_case_table_names=1 character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' innodb_buffer_pool_size = 128M innodb_log_file_size = 128M innodb_file_per_table = 1 innodb_flush_log_at_trx_commit = 0 key_buffer_size = 16M log-error = /data/mysql-5.7.21/log/mysql_error.log log-bin = /data/mysql-5.7.21/log/mysql_bin.log slow_query_log = 1 slow_query_log_file = /data/mysql-5.7.21/log/mysql_slow_query.log long_query_time = 5 tmp_table_size = 16M max_heap_table_size = 16M query_cache_type = 0 query_cache_size = 0 server-id=1 --初始化 # mysqld --initialize --user=mysql --basedir=/data/mysql-5.7.21 --datadir=/data/mysql-5.7.21/data 此时会生成一个root临时登录密码,在mysql_error.log文件, # grep 'temporary password' /data/mysql-5.7.21/log/mysql_error.log 注:有可能并没有写进文件,直接在执行后的 界面 显示 --配置服务 # cp /data/mysql-5.7.21/support-files/mysql.server /etc/init.d/ # mv /etc/init.d/mysql.server /etc/init.d/mysqld 修改mysqld文件 # vi /etc/init.d/mysqld 将basedir和datadir值修改如下,46和47行 basedir=/data/mysql-5.7.21 datadir=/data/mysql-5.7.21/data 加入服务 # chkconfig --add mysqld 设置开机自动启动和查看 # chkconfig mysqld on # chkconfig mysqld --list 启动服务 # service mysqld start 查看mysql服务状态 # service mysqld status --修改mysqlroot密码 # mysql -uroot -p临时密码 mysql> ALTER USER 'root'@'localhost' IDENTIFIED BY '新密码'; mysql> flush privileges; mysql> exit 重新使用修改后的密码登录 # mysql -uroot -p 然后输入临时密码 mysql> show databases; +--------------------+ | Database | +--------------------+ | information_schema | | mysql | | performance_schema | | sys | +--------------------+ 4 rows in set (0.00 sec) 如果能查出如上的信息,表示安装成功。
「深度学习福利」大神带你进阶工程师,立即查看>>>
unsigned和zerofill unsigned是指的无符号,就是没有负数,只能用正数和0,此时unsigned修饰的字段的值可以达到的最大值是原来的最大值加1,即原来一个tinyint字段能表示的范围是:-128~127,而unsigned修饰的字段范围是:0~255。 zerofill是指的一个整型字段在允许的最大位数之前用0填充,比如一个字段设置为tinyint(4),需要用4个数字表示,而tinyint最大值为(假如是unsigned修饰)255,那么前面不足4位数都将用0填充,例如:0005, 0010, 0100, 0255。无论是int或是tinyint等整型数据,括号里面的数字只是代表这个数据最后的显示位数,不影响数据本身的存储大小。就是说int(1)和int(4)数据存储长度一样,int(1)还是会比tinyint(8)存储空间占用大。总结一下数据大小: tinyint(1)与tinyint(4)在占用空间大小上没区别,都是一个字节 int(1)与int(4)在占用空间大小上没区别,都是四个字节 int(1)比tinyint(4)占用空间大,但是对于zerofill修饰的字段,最终显示出来的tinyint(4)都是以4个数字(前面补0)呈现出来,而int(1)是不用0补齐来呈现的 tinyint一个字节 smallint 两个字节 MEDIUMINT三个字节 int 4个字节 BIGINT 8个字节,因此在选择字段类型的时候,在够用的情况下,能选择小的就尽量选择小的,节省更多的空间 int(6)与varchar(6)这两个6所表示的含义是不一样的,int(6)只会影响zerofill字段最终的显示长度,而varchar(6)会要求字符串存储的长度不超过6字节
字段类型说明
各种字段类型在数据库存放的空间占用大小如下表
数据类型 默认范围 unsigned范围 tinyint -2^7 ~ 2^7-1 0 ~ 2^8-1 smallint -2^15 ~ 2^15-1 0 ~ 2^16-1 MEDIUMINT -2^23 ~ 2^23-1 0 ~ 2^24-1 int bigint -2^31 ~ 2^31-1 -2^63 ~ 2^63-1 0 ~ 2^32-1 0 ~ 2^64-1
「深度学习福利」大神带你进阶工程师,立即查看>>> 1. 首先确实是否已安装gcc yum search gcc-c++ 2. 如果未安装,则先安装gcc yum install gcc-c++ 3. 下载redis安装包,可以网络下载,也可以本地上传 wget http://download.redis.io/releases/redis-5.0.5.tar.gz 4. 解压压缩包 tar -zxvf redis-5.0.5.tar.gz 5. 移动文件夹到 /usr/local/ 目录下 mv ./redis-5.0.5 /usr/local/redis 6. 安装,切换到/usr/local/redis目录下 make 7. 修改配置文件 redis.conf,设置daemonize属性为yes,使得redis以守护进程的方式运行。 vi redis.conf 8. 启动redis,并指定配置文件。 /usr/local/redis/src/redis-server /usr/local/redis/redis.conf
「深度学习福利」大神带你进阶工程师,立即查看>>> 1.闲着无聊在客户一套较为重要的系统中提取了一个4天的AWR,发现下面这条SQL产生最多的物理读,四天运行了159次。平均每次物理读1G左右。SQL运行时间三分钟左右。 SQL: SELECT DECODE(B.DH, 'MAIN', :1, B.DH) DH, COUNT(DISTINCT(A.PROCESSID)) VALUE, B.MC COMPANYNAME FROM FMIS3000.WF_ACTIVITIES A, (SELECT DH, MC FROM FMIS3000.XTDW WHERE 1=1 ) B WHERE A.ACTTYPE = 1 AND TRIM(A.DH) = TRIM(B.DH) GROUP BY B.DH, B.MC; 2.我查看了执行计划: PLAN_TABLE_OUTPUT Plan hash value: 2953189885 | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | | 0 | SELECT STATEMENT | | 191K| 18M| | 300K (1)| 01:10:12 | | 1 | SORT GROUP BY | | 191K| 18M| 4006M| 300K (1)| 01:10:12 | |* 2 | HASH JOIN | | 37M| 3516M| | 45650 (1)| 00:10:40 | | 3 | TABLE ACCESS FULL| XTDW | 528 | 18480 | | 4 (0)| 00:00:01 | |* 4 | TABLE ACCESS FULL| WF_ACTIVITIES | 7054K| 430M| | 45478 (1)| 00:10:37 | PLAN_TABLE_OUTPUT Predicate Information (identified by operation id): 2 - access(TRIM("A"."DH")=TRIM("DH")) 4 - filter("A"."ACTTYPE"=1) 17 rows selected. 3.我注意到有两个全表扫描,于是我查找执行这个SQL时 两个表会返回的数据量 这是一个HASH连接 FMIS3000.XTDW 表为驱动表,可以看到他数据量不大,作为驱动表还是很合适的,走table access full,也比较合理。 SQL> select count(*) from FMIS3000.XTDW ; COUNT(*) 530 4.这是被驱动表,数据量比较大。我发现这儿应是一个瓶颈。 SQL> select count(*) from FMIS3000.WF_ACTIVITIES where ACTTYPE=1; COUNT(*) 12078901 5.这个查询返回1.2KW数据,整个表有1.7KW数据。走全表扫描其实并没有错,但是我在想如何解决这个瓶颈 6.接着我返回去看SQL,发现我不需要扫描整个表的数据: 经过查询这个表有26列,我可以在需要的列上面创建索引。 create index id_tex on FMIS3000.WF_ACTIVITIES(ACTTYPE,TRIM(DH),PROCESSID); 在这儿我没有考虑选择性(因为不需要,而且选择性肯定奇差),我推测这样,可以走INDEX FAST FULL SCAN,且不用回表。将会有效提升效率。 建立索引前: PLAN_TABLE_OUTPUT SQL_ID 4n6ajf2fsm2x9, child number 0 SELECT DECODE(B.DH, 'MAIN','MAIN', B.DH) DH, COUNT(DISTINCT(A.PROCESSID)) VALUE, B.MC COMPANYNAME FROM FMIS3000.WF_ACTIVITIES A, (SELECT DH, MC FROM FMIS3000.XTDW WHERE 1=1 ) B WHERE A.ACTTYPE = 1 AND TRIM(A.DH) = TRIM(B.DH) GROUP BY B.DH, B.MC Plan hash value: 2953189885 | Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads | Writes | | 1 | SORT GROUP BY | | 1 | 191K| 475 |00:02:51.39 | 285K| 423K| 138K| |* 2 | HASH JOIN | | 1 | 37M| 12M|00:01:12.56 | 285K| 285K| 0 | | 3 | TABLE ACCESS FULL| XTDW | 1 | 528 | 530 |00:00:00.02 | 10 | 8 | 0 | |* 4 | TABLE ACCESS FULL| WF_ACTIVITIES | 1 | 7054K| 12M|00:00:48.38 | 285K| 285K| 0 | Statistics 1082 recursive calls 71 db block gets 285137 consistent gets 311743 physical reads 0 redo size 2208 bytes sent via SQL*Net to client 514 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 0 sorts (memory) 1 sorts (disk) 30 rows processed 建立索引后: PLAN_TABLE_OUTPUT SQL_ID g5pavm81dbpdt, child number 0 SELECT DECODE(B.DH, 'MAIN', 'MAIN', B.DH) DH, COUNT(DISTINCT(A.PROCESSID)) VALUE, B.MC COMPANYNAME FROM FMIS3000.WF_ACTIVITIES A, (SELECT DH, MC FROM FMIS3000.XTDW WHERE 1=1 ) B WHERE A.ACTTYPE = 1 AND TRIM(A.DH) = TRIM(B.DH) GROUP BY B.DH, B.MC Plan hash value: 513252325 | Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | | 0 | SELECT STATEMENT | | 191K| 18M| | 341K (2)| 01:08:17 | | 1 | SORT GROUP BY | | 191K| 18M| 4062M| 341K (2)| 01:08:17 | |* 2 | HASH JOIN | | 37M| 3516M| | 39892 (2)| 00:07:59 | | 3 | TABLE ACCESS FULL | XTDW | 528 | 18480 | | 4 (0)| 00:00:01 | |* 4 | INDEX FAST FULL SCAN| TE_DX | 7054K| 430M| | 39524 (2)| 00:07:55 | Statistics 1074 recursive calls 51 db block gets 179736 consistent gets 124315 physical reads 0 redo size 2208 bytes sent via SQL*Net to client 514 bytes received via SQL*Net from client 3 SQL*Net roundtrips to/from client 0 sorts (memory) 1 sorts (disk) 30 rows processed 这个执行计划是真实的 可以很明显的看到,不管是逻辑读和物理读都少了一倍左右。(我在执行前都flush了share pool和buffer cache) 建立索引之后 CBO自动选择了INDEX FAST FULL SCAN ,而且确实没有回表。和预估的结果一样。 总的执行时间减少了一倍,在扫描WF_ACTIVITIES表阶段 时间从48秒减少到了8秒。性能提升了6倍。 但是这个SQL还有个瓶颈是SORT GROUP BY 也就是DISTINCT排序导致的。 我也试验了,如果不要distinct,执行时间还可以减少30多秒,这个和业务相关了,就没有太大办法,而且被驱动表返回结果1.2KW行,hash不可能不消耗时间,这个确实没办法。如果知道业务或许可以改写SQL。
「深度学习福利」大神带你进阶工程师,立即查看>>> 在业务查询中经常会出现【过滤出最新一条记录】的需求,下面写一个用【NOT EXISTS】实现的小例子记录一下: SELECT t1.unit_id, IFNULL( COUNT(distinct t.person_phr_code ), 0 ) AS visit_person_num, IFNULL( COUNT(distinct CASE WHEN t.avg_sbp < 130 AND t.avg_dbp < 80 THEN t.person_phr_code ELSE NULL END ), 0 ) AS person_num_80_130, IFNULL( COUNT(distinct CASE WHEN t.avg_sbp < 140 AND t.avg_dbp < 90 THEN t.person_phr_code ELSE NULL END ), 0 ) AS person_num_90_140 FROM yida_historical_visit_info t, yida_person_basic_info t1 WHERE t.person_phr_code = t1.person_phr_code AND NOT EXISTS ( SELECT 1 FROM `yida_historical_visit_info` n WHERE t.person_phr_code = n.person_phr_code AND t.visit_date < n.visit_date) GROUP BY t1.unit_id
「深度学习福利」大神带你进阶工程师,立即查看>>> NSERT IGNORE 与INSERT INTO的区别就是INSERT IGNORE会忽略数据库中已经存在 的数据,如果数据库没有数据,就插入新的数据,如果有数据的话就跳过这条数据。这样就可以保留数据库中已经存在数据,达到在间隙中插入数据的目的。 eg: insert ignore into table(name) select name from table2 mysql中常用的三种插入数据的语句: insert into表示插入数据,数据库会检查主键(PrimaryKey),如果出现重复会报错; replace into表示插入替换数据,需求表中有PrimaryKey,或者unique索引的话,如果数据库已经存在数据,则用新数据替换,如果没有数据效果则和insert into一样; REPLACE语句会返回一个数,来指示受影响的行的数目。该数是被删除和被插入的行数的和。如果对于一个单行REPLACE该数为1,则一行被插入,同时没有行被删除。如果该数大于1,则在新行被插入前,有一个或多个旧行被删除。如果表包含多个唯一索引,并且新行复制了在不同的唯一索引中的不同旧行的值,则有可能是一个单一行替换了多个旧行。 insert ignore表示,如果中已经存在相同的记录,则忽略当前新数据;
「深度学习福利」大神带你进阶工程师,立即查看>>> 《伟大的博弈》读后感作文4100字: 五一小长假,作为工厂员工,没能跟上大家放假的步伐,窝家里两天没出门,逼着自己看完《伟大的博弈》,感触颇多。似乎不能消化,先记录第一遍作为幼稚的见证。 科技进步,人类发展,但人性永恒不变。那些刻在基因里的代码,引领我们走出了非洲丛林,从猿猴变成人。但是,也正是因为这些基因里的代码,让我们不断重复曾今的过错。每一次都会以为这次不一样,但是这次真的不一样吗? 这本书的书名,起得非常的经典,用一个词完美的概括了美国的金融史。博弈,并且是伟大的博弈。是的,人类的存活是关乎于选择的问题,在不同的选择面前,通过各种利益权衡,最终形成了这样的我们。这个博弈的过程,有自然的选择,也有人类的文明选择。资本市场也一样,在博弈的过程中,逐渐发展壮大并完善。从无序到有序,从自由到监管,从传统到创新,都是一系列博弈的过程。 看不见的手与看得见的手 从1653到2019,美国的资本市场从无到有,从路边交易到交易所,从纽约到全球。这个过程,最大的博弈方,就是看不见的手和看得见的手之间的博弈。市场这只看不见的手,具有自我调节作用,在人类社会进步中发挥着重要的作用,甚至是主要的作用。 因为有市场交换的存在,才使得分工与规模化生产得以实现。一个人再也不用掌握所有的生存技能,反而只要掌握自己擅长的技能,并使其专业化,便可以活得更好。 因为分工促使专业化形成,促使规模化生产得以实现。但是市场并不完美,调节具有滞后性,也有短视性,所以伴随着经济的发展,人口规模的扩大,总会有那只看得见的手产生,并积极实施影响,甚至想取代那只看不见的手。比如计划经济。 要自由还是要监管?仅仅看历史的发展,我们也许会发生监管更多了,从《梧桐树协议》到《证券法》在到后面的一系列法案,看似看得见的手更强大了。每次金融危机发生后,是任其自由还是积极救市,总是有不同的声音。甚至连总统层面都有这样的声音。 亚历山大.汉密尔顿将军的积极救市,成立中央银行的措施,良好的控制住恐慌。但是托马斯.杰斐逊总统称华尔街为人性的大阴沟,不仅关闭了中央银行,红楼梦读后感(http://www.simayi.net/duhougan/8051.html)还偿还了所有的债务。后来的安德鲁.杰克逊,执政党在促进金融市场的发展和抑制其发展,反复多次博弈,最终形成现在的纽约金融中心。 早年的胡佛总统在1929年的经济危机中,处理失当。在1000多名经济学联名家反对声中,签署了《斯幕特-霍利关税法案》。并且在危机刚刚发生时,在白宫召开会议,反而引起市场更大的恐慌。(P417) 当年不但没有缓解危机,反而带入更深的泥潭。最近得到包刚升老师的《政治学通识30讲》,讲这件事情是因为胡佛政府的那只看得见的手,想要发挥作用,但是忘记了市场那只看不见的手的反作用。说看得见的手,一旦存在,总是想要发挥作用,所以不仅仅是因为他有权利管,而是他的位置相让他管,甚至很多时候,这只手闲不住。胡佛总体也因此被认为是历史上最差的总体。 不管怎么样,我们总体可以看出,在自由与监管的博弈中,市场更加完善,功能更加齐全,更好的促进了经济的发展,把蛋糕做大,让更多人获得了更多的财富,并且能参与其中。 现今,金融不只是少数人的盛宴,而是只要你想,你有能力,一部手机就可以加入金融这个资本大市场。不过不要忘记了,只要有博弈,就会有输赢。不管监管多么的完善,不管市场多么的有效,只要存在不确定性,就会有波动,就是有机会,也就会有盈亏。 监管是为了更大的自由。 市场并不看重你是谁,仅仅看中你是否足够聪明及幸运。比如总体,能在南北战争中带领北方军队获胜,并且成为总统,但是最后在纽约的资本市场中,亏得只剩下200美元,最后只能靠写传记补贴家人,当然,也因此写成了最伟大的传记之一《格兰特将军回忆录》。 博弈,不仅仅是在自由市场与监管之间,还在于投资者与融资中之间,管理者与所有者之间,公共利益,国与国的博弈,等等。 货币可控性与金融危机 从全书可以看出,危机更多情况下,是由于市场的恐慌情绪引发的。如果在危机发生后,能积极救市,给予市场信心,那么危机的破坏就没那么大。 情绪是难以控制的。比如2019年的今天,A股下跌的势头非常的强劲,哪怕一大早就有定向降准的通知,市场依然下跌。事情的导火索仅仅是川普的一个推文,不见得会实锤。金融,是多方博弈的结果。是科学,也是艺术,有更多不确定性因素来自于参与者本身。 非纸币作为主要流通或法定货币的时期,具有供给与需求的双向不确定性,使市场处于被动的波动中。特别是当发现或者生产更多的贵金属,或者金属本身作为原材料需求上升,总有投机者试图操纵市场。 书里就有两次金属货币操纵风波,一次黄金,一次白银。贵金属作为货币的不确定性,以及携带计量的不方便性,在技术进步中,逐渐被纸币取代,也是必然之势。而今,电子货币的兴起,方便快捷,货币总量也有范围可行。一个强势但又有克制的央行,就显得非常有必要。 而更有意思的是,有一本《银线:19世纪的世界与中国》的书。讲清朝后期,中国闭关锁国,连历史书上也是这样讲。但是实际情况是,我们所用的银子不是中国生产的,而是靠西班牙从墨西哥进口。这样一来,虽然实体上的大门被封锁,但是货币靠外国的时候,中国早已是世界中的一部分,但又没主动参与,并弄清情况,导致了后面一系列的悲剧发生。 货币做为交易的媒介,是价值的体现形式。曾今货币的发现标的是其背后的贵金属,后来为石油。未来,是一个国家的信用、经济走势等等综合国力及大国担当力。 未来,货币将变得越来越复杂。因为资金的便利性、流动性,如果经济形势不好,定价体系混乱,钱会从各种渠道流进/流动,变得难以琢磨。当那只看不见的手,变得越来越强大的时候,看得见的手,如果不能一并成长,危机指日可待。 未来的社会,看得见的手,要变得更加聪明,更有担当,才能维持金融界的暂时稳定。经济社会,没有一劳永逸,总有一些不确定的因子,推动者社会的进步。 资本+科技,经济的腾飞 纽约的经济地位,不是一天造就的。良好的利用了资本+科技,通过科技的进步推进了社会的进步,做大蛋糕的总量,给资本家带来大量的财富和社会地位,也同时造福利大量的人类。迄今,中国贫困人口逐步下降。曾今生活在云南乡村的我,得以坐在广东省的一个三线城市,吹着空调看书,不得不承认是经济的繁荣与发展带来的结果,跟自身努力没多大关系。 15世纪文艺复兴,印刷术的进步,让印刷成本大大降低,从而使信息的传播变得低廉,知识也可以被大众获得并掌握。 16世纪,蒸汽机的改良,让铁路运输变得可行,西部的物资大量运输到东部,也造就了规模化生产的可行性。 纽约的伊利运河的开凿成功,连接了五大湖和纽约地区,让货运成本大幅降低,促进了纽约的繁荣。 后期的电器、汽车、通行、航天航空,更进一步推进了经济的发展和科技的进步。 纽约成为全球最大的金融中心,与紧跟科技技术发展分不开。从曾今的runner跑单交易,到后来的电报,再到大西洋电缆线,让纽约跟欧洲相连接,引入欧洲资本,促进了纽约的进一步繁荣。 21世纪,互联网的发展,以苹果、微软、亚马逊、google、facebook、airbnb为代表的科技巨头的崛起,就是极好的资本+科技的发展例子。资本让科技更快速的落地,更进一步的推动人类社会的进步。 如今,资本家跟以前一样贪婪,但是因为有了更快、更大、更全的载体,科技进步的速度超越我们的想象。资本和产业,也联系得更加的紧密,彼此难以分开。虽然资本家依然贪婪,参与者也一样恐惧,人性,其实也没多大的改变,但是因为科技的进步,让蛋糕变大。如果更好的利用资本,将会带来正向的发展。如果不敬畏市场,不好好利用资本和市场的力量,走不出大草原期的行为模式,苦难不断。 政治经济学是否为变为经济政治学 2016年特朗普当选美国总统。2017年马克龙当选法国总统。这两个人的共同特点,就是没很强的政治经验。一个是美国商界大亨,一个是法国的经济顾问。他们的当选,很多人觉得不可思议。但是现在回头看,通过书中的观点,他们至少都应该懂经济。参选的目的也是带领经济腾飞。对于普通百姓、中产阶级,这是一个利好的面,不当选都不可能。 2008年美国的次贷危机,如果简单粗暴的归咎于资本家的贪婪,真的有点不负责任。从该书作者的观点,我们至少可以追溯到罗斯福新政,然后到克林顿修正的《社区再投资法》。产生了房地美和房利美这样的奇拉美——-希腊神话中的狮头、羊身、蛇尾的怪物。不熟悉经济运作规律,但又有政治资本,能获大量的补贴。这样盈亏不需要自负,还可以不断获得补贴奖金的机构,没有KPI业绩考核,自然不会对市场产生敏锐的嗅觉,也不会主动防范风险,聚集大量的风险。 从2003年开始,小布什就要求国会对“两房”实施更加严格的监管和监督。美联储主席格林斯潘也持有相同观点。但是即提示有风险,但依然错过这个修正机会。 奥巴马能连任,但是他执政期间,经济疲软,一系列法案,明显的违背经济规律。特朗普执政的3年间,不断修正/废除一些法案。未来是否还有更多的法案存在潜在风险,不得而知。但是,未来,政治经济学也许应该改名为经济政治学?多点经济规律,让经济与政治完美结合,致力于做大蛋糕,促使人类文明进一步发展。 初读此书,买空卖空、对敲交易、期权期货,是早在大航海时代就开始的交易手段,至今仍然是主要的交易手段。一个完善的交易环境,一个有效的市场,总是能促进经济的繁荣。当然其中也有很多运气的成分,比如三次危机中,都发生了战争。为了生产足够的军需产品,筹措战争款项,各种金融产品及工具被开发、被创新,引领经济走出萧条。也慢慢奠定了纽约的金融中心的位置。未来,这样的运气,当然不会是战争。但也许是科技的进步,认知模型的升级,新能源的发现,等等,不得而知。从1653到2019,366年间,财富总体是正向飞速增长的。作为个体,跟上经济发展的节奏,一直进步,总能享受经济繁荣带来的果实。 中国股市从1990年到2019年,仅仅29年。很多知识和经验更多来源与西方。未来,通过自己的积累与完善,地位提升,影响加大,共享经济繁荣带来的果实。 未来,依然充满不确定性。自律+学习,提升认知,认清博弈的世界,并积极参与!
「深度学习福利」大神带你进阶工程师,立即查看>>> MySQL 存储过程简单实例 ============================== 1、创建实例数据库 CREATE DATABASE procdb DEFAULT CHARSET utf8; USE procdb; ============================== 2、创建要使用的表 CREATE TABLE tb ( id INT PRIMARY KEY NOT NULL, cname VARCHAR(20) ); INSERT INTO tb VALUES (1,'统一方便面'), (2,'康师傅方便面'), (3,'马老表方便面'), (4,'王老吉方便面'); SELECT * FROM tb; ============================== 3、创建一个最简单的存储过程 DELIMITER $$ CREATE PROCEDURE p1() BEGIN SELECT id,cname FROM tb; END$$ DELIMITER ; #调用存储过程p1 CALL p1(); ============================== ################ DELIMITER $$ CREATE PROCEDURE p2() BEGIN #declare @dt int; ##使用临时变量不用声明数据类型 SET @dt =CURDATE(); SELECT CONCAT(YEAR(@dt),'年',MONTH(@dt),'月',DAY(@dt),'日') ; END$$ DELIMITER ; #调用存储过程P2 CALL p2(); ============================== 4、创建一个带传入参数的存储过程 DROP PROCEDURE IF EXISTS p3; DELIMITER $$ CREATE PROCEDURE p3(IN aid INT) COMMENT '向表中插入一条记录' BEGIN ##定义一个整形变量 DECLARE v1 INT; ##将输入参数的值赋给变量 SET v1 = aid; ##执行插入操作 INSERT INTO tb(id,cname) VALUES(v1,CONCAT('第',v1,'条记录')); END$$ DELIMITER ; ##调用存储过程,使用一个参数,就会在表中插入一条记录 CALL p3(99); SELECT * FROM tb; ============================== 5、带输出参数的存储过程 DROP PROCEDURE IF EXISTS p4; DELIMITER $$ CREATE PROCEDURE p4(OUT ret INT) COMMENT '输出表中id的最大值' BEGIN DECLARE maxid INT; SELECT MAX(id) INTO maxid FROM tb; SET ret=maxid; END$$ DELIMITER ; #调用存储过程 CALL p4(@maxno); SELECT @maxno; #查看临时变量@maxno ============================== 6、带输入输出参数的存储过程 DROP PROCEDURE p5; DELIMITER $$ CREATE PROCEDURE p5(IN p1 INT , OUT p2 INT) COMMENT '带输入输出参数的存储过程' BEGIN IF p1 = 1 THEN #用@符号加变量名的方式定义一个变量,与declare类似 SET @v = 10; ELSE SET @v = 20; END IF; #语句体内可以执行多条sql,但必须以分号分隔 INSERT INTO tb(id,cname) VALUES(@v,CONCAT('第',@v,'条记录')); SELECT MAX(id) INTO p2 FROM tb; END$$ DELIMITER ; #调用存储过程 CALL p5(2,@maxid); SELECT * FROM tb; SELECT @maxid; ============================== 7、既做输入又做输出参数的存储过程 DROP PROCEDURE IF EXISTS p6; DELIMITER $$ CREATE PROCEDURE p6(INOUT argument INT) COMMENT '既做输入又做输出参数的存储过程' BEGIN IF argument = 4 THEN SET @pg = 400; ELSE SET @pg = 500; END IF; SELECT @pg; SET argument=@pg; END$$ DELIMITER ; # 调用存储过程,这里需要先设置一个已赋值的变量,然后再作为参数传入,同时也作为输出参数 SET @pp = 5; CALL p6(@pp); SELECT @pp;
「深度学习福利」大神带你进阶工程师,立即查看>>> 回收站(Recycle Bin)从原理上来说就是一个数据字典表,放置用户删除(drop)掉的数据库对象信息。用户进行删除操作的对象并没有被数据库删除,仍然会占用空间。除非是由于用户手工进行Purge或者因为存储空间不够而被数据库清掉。 如果一个表被drop删除,那么与表关联的对象、约束条件、索引都会一并删除。 2种查看回收站的当前状态:(默认是打开的) SQL> show parameter bin NAME TYPE VALUE cursor_bind_capture_destination string memory+disk recyclebin string on SQL> SELECT Value FROM V$parameter WHERE Name = 'recyclebin'; VALUE ON 启动或者关闭回收站里的每个会话(session)和系统(system),代码如下: ALTER SYSTEM SET recyclebin = ON; ALTER SESSION SET recyclebin = ON; ALTER SYSTEM SET recyclebin = OFF; ALTER SESSION SET recyclebin = OFF; 注:执行ALTER SESSION SET recyclebin = OFF只适用于当前会话窗口,重新复制新窗口默认回收站依旧是打开的,测试如下: SQL> ALTER SESSION SET recyclebin = OFF; Session altered. 新开窗口: [oracle @strong ~]$ sqlplus / as sysdba SQL*Plus: Release 11.2.0.4.0 Production on Thu Jan 28 18:03:00 2018 Copyright (c) 1982, 2013, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - Production With the Partitioning, OLAP, Data Mining and Real Application Testing options SQL> show parameter bin NAME TYPE VALUE cursor_bind_capture_destination string memory+disk recyclebin string on 修改system系统级别的测试如下: SQL> ALTER SYSTEM SET recyclebin = OFF; ALTER SYSTEM SET recyclebin = OFF * ERROR at line 1: ORA-02096: specified initialization parameter is not modifiable with this option 报错:不允许这样操作 原因:回收站是数据文件里面的动态参数,需要添加spoce=spfile,重启数据库才能修改成功 测试如下: SQL> alter system set recyclebin=off scope=spfile; System altered. [root @strong ~]# reboot Broadcast message from root @strong (/dev/pts/5) at 18:05 ... The system is going down for reboot NOW! [root @strong ~]# su - oracle [oracle @strong ~]$ sqlplus / as sysdba SQL*Plus: Release 11.2.0.4.0 Production on Thu Jan 18 18:10:29 2018 Copyright (c) 1982, 2013, Oracle. All rights reserved. Connected to: Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - Production With the Partitioning, OLAP, Data Mining and Real Application Testing options SQL> show parameter bin NAME TYPE VALUE cursor_bind_capture_destination string memory+disk recyclebin string OFF 注:生产线上建议把回收站设置为on打开,当我们有误操作删除表时,还可以到回收站找回(以上只是我本人的测试,不建议在生产线操作)。
「深度学习福利」大神带你进阶工程师,立即查看>>> 1、根据查询语句创建表 CREATE table <表名称> as > 2、根据字段名查询重复行 SELECT * FROM <表名称> GROUP BY 字段1, 字段2, ... , 字段n HAVING count(1) > 1; 3、去除重复行并且只保存一条 select * from <表名> where <目标字段> exits (SELECT <目标字段> FROM <表名称> GROUP BY 字段1, 字段2, ... , 字段n HAVING count(1) > 1) and <表名>.id not in (SELECT min(<表名>.id) FROM <表名称> GROUP BY 字段1, 字段2, ... , 字段n HAVING count(1) > 1); 这个sql查询出来的是所有要删除的重复列,可以用查询出来的重复列做一个临时表,去删除目标表的重复数据
「深度学习福利」大神带你进阶工程师,立即查看>>> Cloud Toolkit 是一个 IDE 插件,帮助开发者更高效地开发、测试、诊断并部署应用。在最新版的插件中,提供了快速创建 Dubbo 工程的功能,下面就来快速体验下吧。 Dubbo 采用全 Spring 配置方式,透明化接入应用,对应用没有任何 API 侵入,只需用 Spring 加载 Dubbo 的配置即可,Dubbo 基于 Spring 的 Schema 扩展 进行加载。 如果不想使用 Spring 配置,可以通过 API 的方式 进行调用。 功能预览 功能入口 打开 IntelliJ IDEA,进入菜单: File - New - Project... 第一步:选择 JAVA SDK 版本 第二步:填写应用基本信息 ** 包括选择 Dubbo 版本、Spring Boot 版本等。 第三步:确定创建 如下图所示,就完成了一个完整的 Dubbo 工程的创建了,此工程的结构和Apache Dubbo 官方样例工程完全一致。 参考 Apache Dubbo 官方样例工程: https://dubbo.apache.org/zh-cn/docs/user/quick-start.html 亮点1:本地应用一键部署 Deploy to ECS 开发者本地编写的应用程序,在图形化界面上进行配置,即可持续便利的部署到云端的 ECS 服务器上;在 Eclipse 中完成编码后,无须在 Maven 、Git 以及其他运维脚本和工具的之间切换,借助 Cloud Toolkit for Eclipse 插件,在 IDE 的图形界面上选择一个或若干个 ECS 实例,即可将应用程序部署至 ECS 指定目录 Deploy to EDAS 针对阿里云 EDAS 产品的开发者,我们也在插件上打通了本地应用程序和云端部署,在 Eclipse 中完成编码后,将 IDE 内的项目工程,关联上 EDAS 的应用,即可实现快速部署。 Deploy to CS Kubernetes 针对阿里云 容器服务 Kubernetes 产品的开发者,我们也在插件上打通了本地应用程序和云端Kubernetes部署,在 Eclipse 中完成编码后,将 IDE 内的项目工程,关联上 容器服务 Kubernetes 的部署,即可实现快速部署。 亮点2:内置终端 Terminal IDE 内,开发者可以直接通过内置的终端 Terminal,快速登录远程服务器; 对了,不仅仅用于阿里云服务器 ECS,所有支持标准 SSH 协议的机器,都可以 亮点3:文件上传 Cloud Toolkit 帮助开发者在 IDE 内,一键将本地或者远程 URL 文件上传到服务器指定目录下去,无需在各种 FTP、SCP 工具之间频繁切换 更为重要的是,文件上传完毕后,还支持命令执行,比如:文件解压缩、程序启动等 作者:银时 原文链接 本文为云栖社区原创内容,未经允许不得转载。
「深度学习福利」大神带你进阶工程师,立即查看>>> 1. 简介 在数据库SQL处理中,常常有行转列(Pivot)和列转行(Unpivot)的数据处理需求。本文以示例说明在Data Lake Analytics( https://www.aliyun.com/product/datalakeanalytics )中,如何使用SQL的一些技巧,达到行转列(Pivot)和列转行(Unpivot)的目的。另外,DLA支持函数式表达式的处理逻辑、丰富的JSON数据处理函数和UNNEST的SQL语法,结合这些功能,能够实现非常丰富、强大的SQL数据处理语义和能力,本文也以JSON数据列展开为示例,说明在DLA中使用这种SQL的技巧。 2. 行转列(Pivot) 2.1 样例数据 test_pivot表内容: +------+----------+---------+--------+ | id | username | subject | source | +------+----------+---------+--------+ | 1 | 张三 | 语文 | 60 | | 2 | 李四 | 数学 | 70 | | 3 | 王五 | 英语 | 80 | | 4 | 王五 | 数学 | 75 | | 5 | 王五 | 语文 | 57 | | 6 | 李四 | 语文 | 80 | | 7 | 张三 | 英语 | 100 | +------+----------+---------+--------+ 2.2 方法一:通过CASE WHEN语句 SQL语句: SELECT username, max(CASE WHEN subject = '语文' THEN source END) AS `语文`, max(CASE WHEN subject = '数学' THEN source END) AS `数学`, max(CASE WHEN subject = '英语' THEN source END) AS `英语` FROM test_pivot GROUP BY username ORDER BY username; 结果: +----------+--------+--------+--------+ | username | 语文 | 数学 | 英语 | +----------+--------+--------+--------+ | 张三 | 60 | NULL | 100 | | 李四 | 80 | 70 | NULL | | 王五 | 57 | 75 | 80 | +----------+--------+--------+--------+ 2.3 方法二:通过map_agg函数 该方法思路上分为两个步骤: 第一步,通过map_agg函数把两个列的多行的值,映射为map; 第二步,通过map的输出,达到多列输出的目的。 第一步SQL: SELECT username, map_agg(subject, source) kv FROM test_pivot GROUP BY username ORDER BY username; 第一步输出: +----------+-----------------------------------+ | username | kv | +----------+-----------------------------------+ | 张三 | {语文=60, 英语=100} | | 李四 | {数学=70, 语文=80} | | 王五 | {数学=75, 语文=57, 英语=80} | +----------+-----------------------------------+ 可以看到map_agg的输出效果。 最终,该方法的SQL: SELECT username, if(element_at(kv, '语文') = null, null, kv['语文']) AS `语文`, if(element_at(kv, '数学') = null, null, kv['数学']) AS `数学`, if(element_at(kv, '英语') = null, null, kv['英语']) AS `英语` FROM ( SELECT username, map_agg(subject, source) kv FROM test_pivot GROUP BY username ) t ORDER BY username; 结果: +----------+--------+--------+--------+ | username | 语文 | 数学 | 英语 | +----------+--------+--------+--------+ | 张三 | 60 | NULL | 100 | | 李四 | 80 | 70 | NULL | | 王五 | 57 | 75 | 80 | +----------+--------+--------+--------+ 3. 列转行(Unpivot) 3.1 样例数据 test_unpivot表内容: +----------+--------+--------+--------+ | username | 语文 | 数学 | 英语 | +----------+--------+--------+--------+ | 张三 | 60 | NULL | 100 | | 李四 | 80 | 70 | NULL | | 王五 | 57 | 75 | 80 | +----------+--------+--------+--------+ 3.2 方法一:通过UNION语句 SQL语句: SELECT username, subject, source FROM ( SELECT username, '语文' AS subject, `语文` AS source FROM test_unpivot WHERE `语文` is not null UNION SELECT username, '数学' AS subject, `数学` AS source FROM test_unpivot WHERE `数学` is not null UNION SELECT username, '英语' AS subject, `英语` AS source FROM test_unpivot WHERE `英语` is not null ) ORDER BY username; 结果: +----------+---------+--------+ | username | subject | source | +----------+---------+--------+ | 张三 | 语文 | 60 | | 张三 | 英语 | 100 | | 李四 | 语文 | 80 | | 李四 | 数学 | 70 | | 王五 | 英语 | 80 | | 王五 | 语文 | 57 | | 王五 | 数学 | 75 | +----------+---------+--------+ 3.3 方法二:通过CROSS JOIN UNNEST语句 SQL语句: SELECT t1.username, t2.subject, t2.source FROM test_unpivot t1 CROSS JOIN UNNEST ( array['语文', '数学', '英语'], array[`语文`, `数学`, `英语`] ) t2 (subject, source) WHERE t2.source is not null 结果: +----------+---------+--------+ | username | subject | source | +----------+---------+--------+ | 张三 | 语文 | 60 | | 张三 | 英语 | 100 | | 李四 | 语文 | 80 | | 李四 | 数学 | 70 | | 王五 | 语文 | 57 | | 王五 | 数学 | 75 | | 王五 | 英语 | 80 | +----------+---------+--------+ 4. JSON数据列展开 JSON数据的表达能力非常灵活,因此在数据库和SQL中,常常需要处理JSON数据,常常碰到稍复杂的需求,就是将JSON数据中的某些属性字段,进行展开转换,转成行、列的关系型表达。 4.1 基本思路和步骤 使用JSON函数,对JSON字符串进行解析和数据提取; 提取、转换为ARRAY或者MAP的数据结构,如有需要,可以使用Lambda函数式表达式进行转换处理; 利用UNNEST语法进行列展开。 下面以多个示例说明。 4.2 用UNNEST对MAP进行关系型展开 SQL示例: SELECT t.m, t.n FROM ( SELECT MAP(ARRAY['foo', 'bar'], ARRAY[1, 2]) as map_data ) CROSS JOIN unnest(map_data) AS t(m, n); 结果: +------+------+ | m | n | +------+------+ | foo | 1 | | bar | 2 | +------+------+ 4.3 用UNNEST对JSON数据进行关系型展开 SQL示例: SELECT json_extract(t.a, '$.a') AS a, json_extract(t.a, '$.b') AS b FROM ( SELECT cast(json_extract('{"x":[{"a":1,"b":2},{"a":3,"b":4}]}', '$.x') AS array) AS package_array ) CROSS JOIN UNNEST(package_array) AS t(a); 结果: +------+------+ | a | b | +------+------+ | 1 | 2 | | 3 | 4 | +------+------+ SQL示例: SELECT t.m AS _col1, t.n AS _col2 FROM ( SELECT cast(json_extract('{"x":[{"a":1,"b":2},{"a":3,"b":4}]}', '$.x') AS array) AS array_1, cast(json_extract('{"x":[{"a":5,"b":6}, {"a":7,"b":8}, {"a":9,"b":10}, {"a":11,"b":12}]}', '$.x') AS array) AS array_2 ) CROSS JOIN UNNEST(array_1, array_2) AS t(m, n); 结果: +---------------+-----------------+ | _col1 | _col2 | +---------------+-----------------+ | {"a":1,"b":2} | {"a":5,"b":6} | | {"a":3,"b":4} | {"a":7,"b":8} | | NULL | {"a":9,"b":10} | | NULL | {"a":11,"b":12} | +---------------+-----------------+ SQL示例: SELECT json_extract(t.m, '$.a') AS _col1, json_extract(t.m, '$.b') AS _col2, json_extract(t.n, '$.a') AS _col3, json_extract(t.n, '$.b') AS _col4 FROM ( SELECT cast(json_extract('{"x":[{"a":1,"b":2},{"a":3,"b":4}]}', '$.x') AS array) AS array_1, cast(json_extract('{"x":[{"a":5,"b":6}, {"a":7,"b":8}, {"a":9,"b":10}, {"a":11,"b":12}]}', '$.x') AS array) AS array_2 ) CROSS JOIN UNNEST(array_1, array_2) AS t(m, n); 结果: +-------+-------+-------+-------+ | _col1 | _col2 | _col3 | _col4 | +-------+-------+-------+-------+ | 1 | 2 | 5 | 6 | | 3 | 4 | 7 | 8 | | NULL | NULL | 9 | 10 | | NULL | NULL | 11 | 12 | +-------+-------+-------+-------+ 4.4 结合Lambda表达式,用UNNEST对JSON数据进行关系型展开 SQL示例: SELECT count(*) AS cnt, package_name FROM ( SELECT t.a AS package_name FROM ( SELECT transform(packages_map_array, x -> Element_at(x, 'packageName')) AS package_array FROM ( SELECT cast(Json_extract(data_json, '$.packages') AS array>) AS packages_map_array FROM ( SELECT json_parse(data) AS data_json FROM ( SELECT '{ "packages": [ { "appName": "铁路12306", "packageName": "com.MobileTicket", "versionName": "4.1.9", "versionCode": "194" }, { "appName": "QQ飞车", "packageName": "com.tencent.tmgp.speedmobile", "versionName": "1.11.0.13274", "versionCode": "1110013274" }, { "appName": "掌阅", "packageName": "com.chaozh.iReaderFree", "versionName": "7.11.0", "versionCode": "71101" } ] }' AS data ) ) ) ) AS x (package_array) CROSS JOIN UNNEST(package_array) AS t (a) ) GROUP BY package_name ORDER BY cnt DESC; 结果: +------+------------------------------+ | cnt | package_name | +------+------------------------------+ | 1 | com.MobileTicket | | 1 | com.tencent.tmgp.speedmobile | | 1 | com.chaozh.iReaderFree | +------+------------------------------+ 作者:julian.zhou 原文链接 本文为云栖社区原创内容,未经允许不得转载。
「深度学习福利」大神带你进阶工程师,立即查看>>> 概述 水平拆分的概念随着分布式数据库的推广已为大部分人熟知。分库分表、异构索引、小表广播、这些功能几乎是产品功能需求标配。然而有些客户使用分布式数据库后的体验不尽如意。 本文尝试从数据的角度总结分布式数据的复制(replication)和分区(partition)技术原理和方案,其中分区也有称为分片(sharding),希望能引起读者一些思考,在分布式数据库选型中能注意这些细节的区别,选择适合业务的数据水平拆分方案。 分布式数据库架构 分布式数据库以集群形式存在,有多个节点。集群架构有共享磁盘架构(shared-disk)和无共享架构(shared-nothing)。后者有时也称为水平扩展(horizontal scale)或向外扩展(scale out),本文主要总结无共享架构方案。 无共享架构的各个节点之间的通信都是软件层面使用网络实现,不同产品在架构不同导致这个细节也不同。有些架构是计算与存储分离。计算节点特点是无状态(即数据不要求持久化),通过集群方式管理,可以水平扩展;存储节点有数据,使用复制和分区技术,节点间任务集中调度或者独立交互。了解这个架构细节都可用性分析会更加具体。具体分布式数据库架构有哪些请参考《 一些关系数据库的架构总结 》。 这里节点的实际体现形式可以是一个机器,也可以是机器上的一个实例。比如说有些数据库支持单机安装多个实例,如MySQL。每个节点具备一定的资源和能力。资源指的是CPU、内存和磁盘,能力是提供数据读写和存储能力。分布式数据库需要把多个节点的能力聚集到一起集中管理,只是不同分布式数据库产品对资源的管理能力各有特点。 在分布式数据库里,数据随处可见,这是最容易让人混淆的地方。因为数据经过复制和分区后会有两种存在形式:副本(replica)和分区(partition)。 数据的复制(replication) 复制(replication)指在几个不同的节点上保存数据的相同副本(replica)。复制提供了冗余的能力。 其作用一是提供高可用能力:如果一个节点不可用,剩余的节点可以快速提供数据服务。作用二是提供读写分离能力。常见的有两副本和三副本架构。 多个副本内容相同,角色会有区分。常见的是一个副本是Leader角色(有的也称主副本),默认提供读写服务;其他副本是Follower角色(有的也称备副本),默认不提供服务。这种架构也称为基于单Leader的(Single Leader-based)。还有其他架构是多Leader的,每个Leader都有数据要复制到其他Leader或Follower,这种架构会有个明显的问题就是数据冲突处理。如果产品层面不处理,用户直接使用风险会很高。 后面讨论的是前者:基于单Leader副本架构。 多副本之间数据同步不是依赖业务多写,而是采用副本间复制事务日志(Redo)技术。复制的方式有同步复制和异步复制。使用同步复制方式,备副本要收到Redo并落盘主副本才能提交,也叫强同步;使用异步复制方式,Follower副本相对Leader副本内容会有延时,具体延时多少取决于Leader副本上事务量、网络传输速度、Follower副本所在节点的负载和能力。强同步的缺点时主副本写性能会下降,同时如果备副本不可用主副本也不能提供服务(变相的解决方案是复制方式降级为异步复制)。 传统关系型数据库还有一种用法一主两备架构,使用同步复制,只要任何一个备副本收到Redo,主副本的事务就可以提交。这个方案优点是保障了数据在多个副本中存在,高可用时有候选副本,也不用担心挂掉一个备副本会影响主副本。它的缺点是不能自动知道哪个候选副本拥有主副本最新最全的数据,也不强制要求两个备副本都要拥有全部数据。 还有一类三副本架构在复制时使用的是Paxos协议,三副本会就Redo落盘事件进行投票,有两个副本成功了Leader副本的事务即可提交。这个表面上跟上面传统一主两备的三副本效果一样,实际上还是有区别的。区别一是使用Paxos协议时,如果Leader副本自身投票慢了,两个Follower副本投票成功,Leader副本的事务也是能提交的;区别二是第三个副本最终也必须写Redo成功,否则其状态就是异常,产品自身可以发现并自动修复(如重新创建一个副本);区别三是使用Paxos协议时,在Leader副本不可用时还可以自动选出新的Leader副本并且拥有老Leader副本的最新数据。这里其实说的是高可用机制。同样,这里对用户而言也不知道哪个Follower副本拥有最新最全的数据,如果访问Follower副本(读写分离),也可能发现数据有延时。 大部分数据库做副本复制使用的是Redo,也称为物理同步。在应用Redo的时候直接是数据块变更。使用物理同步机制的备副本是不提供写服务,不能修改。还有一类复制使用的是Binlog,也称为逻辑同步。Binlog里只包含已提交的事务,并且在应用的时候是通过执行SQL。使用逻辑同步的备副本通常也可能是主副本,可以修改(如MySQL的双向复制架构Master-Master)。如果目标端数据不对,应用SQL会失败,这个复制就会中断需要人介入处理。这也进一步加深了主备副本不一致的概率。 关于副本角色的粒度,有多种实现方案。 传统关系数据库主备架构,主副本或备副本的粒度就是实例。对于主实例(Primary)而言,里面所有数据库(或SCHEMA)的所有表的角色都是主;备实例(Standby)里数据则都是备副本。如果发生高可用切换,业务会中断几十秒或几分钟然后恢复(需要人工处理或自动化脚本处理)。 还有一种粒度是到表。即一个节点内有些表是Leader副本,有些表是Follower副本,这样这个节点就不能简单的说是主节点(实例)或备节点(实例)。这个副本角色细节业务也是可以获取的,如果发生高可用切换,业务会中断十几秒然后恢复。 还有一种粒度是存储级别的定长块。即一个节点的存储里,部分数据块是Leader副本,部分数据块是Follower副本。这种对业务就完全透明,业务基本不感知高可用切换。 数据的分区(partition) 上面总结的是数据的复制(冗余,多副本),对于非常大的数据集(表)或者非常高的访问量(QPS),仅仅靠复制是不够的,还需要对数据进行分区(partition),也称为分片(sharding)。 分区粒度 首先这里的分区(partition)是一种抽象概念,在不同数据库产品里这个体现是不一样的。如在MongoDB, Elasticsearch中体现为分片(shard),在HBase中体现为区域块(Region),Bigtable中体现为表块(tablet),ORACLE中体现为分区(partition),Couchbase中体现为虚拟桶(vBucket)。可见不同的数据库产品数据分区的粒度不同。在分布式关系数据库中间件中,分片的粒度是分表(物理表);在真正的分布式关系数据库里,分片的粒度有分区(partition,同ORACLE)或者区域块(Region)。 分区粒度对业务研发的使用体验影响很大。 比如说中间件常用分库分表方案,使用时对开发和运维会有一些要求。如建很多同构的表并后期维护、要求SQL带上拆分键,还有一些功能限制(如跨库JOIN问题)、底层存储节点用的数据库自身高可用和多副本的数据一致问题等等。不同的中间件产品能力上也有区别,互联网大厂的产品由于内部场景培育很久,做的相对成熟一些。 体验最好的分区粒度就是存储级别的Region,业务研发完全不用关心分片细节,也无法干预分片细节。当有些场景追求性能需要干预数据分布特点时就不好处理。 介入这两种策略之间的就是分区。物理上业务只要创建一个分区表,根据业务特点指定分区策略(包含分区列、拆分算法、分区数目等)。 数据复制是为了冗余和高可用,数据分区主要是为了可扩展性。不管使用哪种分区方案,业务的每条数据(记录)属于且仅属于一个分区(或分片sharding),同一个分区(分片)只会存在于一个节点。前面说了每个节点代表了一定的资源和能力。当复制和分区(分片)一起使用的时候,注意区分你看到的数据。 分区策略 分区的目标是将大量数据和访问请求均匀分布在多个节点上。如果每个节点均匀承担数据和请求,那么理论上10个节点就应该能承担10倍于单节点的数据量和访问量。这个理论是忽略了复制产生的Follower副本的存在。Follower副本的空间和内存是不可能跟其他Leader副本共享的,但是计算能力(CPU)是可以的。当所有节点都提供服务的时候(多活),是计算资源最大利用。 然而如果分区是不均匀的,一些分区的数据量或者请求量会相对比较高,出现数据偏斜(skew),这个可能导致节点资源利用率和负载也不均衡。偏斜集中的数据我们又称为热点数据。避免热点数据的直接方法就是数据存储时随机分配(没有规则)给节点,缺点是读取的时候不知道去哪个分区找该记录,只有扫描所有分区了,所以这个方法意义不大。实际常用的分区策略都是有一定的规则。 这个规则可以是业务规则,也可以不是。 业务规则的分区首先是选取一个或一组列作为分区键,然后选取拆分方法。比如说根据键的范围(Range)分区,分区数量和边界时确定的(后期还可以新增分区)。好处时针对分区键的范围扫描性能会比较好。分布式数据库中间件的分库分表、分区表的分区都支持RANGE 拆分函数。各个产品拆分细节上面会有一些创新。Range分区的缺点是某些特定的访问模式会导致热点。比如说根据时间列做RANGE分区,业务写入和读写数据集中在最近的时间,就可能导致各个分区负载不均衡。这只是一个缺点,业务层面还要考虑这样做的好处。比如说删除历史分区比较快。 还有种拆分方法是散列(HASH)分区,分区数量和边界是确定的(后期可以做分区分裂)。这时各个数据的分布是否均衡就取决于各个产品实现机制。大部分做法是使用一个散列(HASH)函数对Key计算一个值,然后针分段存储。 有的产品会使用这个HASH值对分区数取模,这个方法可能引起分区数据分布不均匀(若MySQL的Key分区)。此外如果要调整分区数,则需要移动所有数据。ORACLE的HASH分区时会先选取最接近分区数的一个2的幂值,对于分区数大于这个值的分区,会从前面分区里调过来。所以ORACLE 建议HASH分区数为2的幂。M有SQL建议Key分区数为奇数时数据分布最均匀。 此外在现有分区下还可以再做一次分区,分区键和分区方法都可以不一样。通常称为两级分区。比如说分库分表时,分库和分表策略不一样就是两级分区;分区表也支持两级分区。 有业务规则的分区方案的特点就是使用上。SQL如果要性能好建议带上分区键,这样分布式数据库才可以直接定位到所访问数据所在的分片;否则,数据库就要扫描所有分区去查询数据。通常分区键只能选取一个或一组业务字段,代表的是一个业务维度,那么另外一种业务维度的SQL请求性能就会不好。个别分布式数据库产品在HASH 方法上支持两种维度的分区列,其前提是在业务构造数据时让这两个列有着内部一致的分区逻辑。 详情可以参考《说说分库分表的一个最佳实践》。 另外一种分区策略就是无业务规则的,在存储级别按块的大小切分为多个定长块(Region)。这个分区对业务而言就是透明的,所以使用体验上会相对好一些。 不过,分布式数据库里的数据分区除了存储数据还要提供读写服务。业务读写数据的SQL本身是带业务逻辑的,如果一次SQL请求访问的数据分散到多个分区,而这些分区又散落在不同的节点上,不可避免的会发生跨节点的请求。如果是多表连接,这种情形更容易出现。如果这个业务请求有事务,那这就产生了分布式事务。分布式事务解决方案有两种,强一致的两阶段提交(XA)方案和最终一致的TCC方案。详情请参考《说说数据库事务和开发(下)—— 分布式事务》。 这里主要提示跨节点的请求带来的性能衰减。当然,硬件方面万兆网卡加RDMA技术下网络延时已经缩小很多,但是当分布式数据库的请求量(QPS)非常高时,或者分布式数据库是多机房部署(比如说两地三中心)时,跨机房的网络延时还是不可忽视,跨节点的请求带来的性能衰减也会很明显。所以有业务规则的分区策略可以提供策略给业务控制自己的数据分区分布特点,非常适合做异地多活和单元化类业务。此外还有个常用的规避跨节点请求读的方法就是小表广播,即将个别没有分区的表的数据复制到其他分区所在的节点,这样相关业务数据分区的JOIN就是在本地节点内部完成。这里就看复制使用的是物理同步还是逻辑同步,以及同步的延时是否满足业务需求。 分区数量 关于分区数量也需要评估。如果是无规则的分区策略,由于每个分区(分片)是定长块,那么分区数量就由总数据大小除以定长块大小,对业务也是透明的。这里总结的是有业务规则的分区的数量。 使用分区的目的是为了扩展性,具体就是能将不同分区分散多多个节点上,发挥多个节点的资源和能力。所以分区数一定要大于可用的资源节点数,为了考虑到将来分布式数据库可能会扩容,分区数应该是数倍于当前规划的节点数。这是一个总的指导思想。由于不同的分布式数据库其节点的表示方法不一样,实施的时候会略有不同。 比如说在分布式数据库中间件架构里,数据存储的节点是实例,数据分区的粒度是分表(物理表),中间还有一层分库的维度。分布式数据库实例:总物理实例数:总物理分库数:总物理分表数=1:M:N:X 。X是分区的数量,N 是总分库数。X 是固定的,如果要调整分区数,成本非常高,所以一般都是提前规划好。N 是总分库数,是2的幂。 M 是实例的数量,也建议是2的幂,决定了最大能用多少节点的资源。 N/M 的结果决定了未来能扩容的倍数。分布式数据库中间件由于数据分区落在具体的节点后就不能自由移动,其扩容方式多是对每个实例一分为二,最好的途径就是利用数据库(MySQL)自身的主从复制搭建新的备实例扩容节点数。 此外分区数还要考虑到单个分区的容量和请求量是否满足需求。即分区是否到位。这个也是需要业务评估的。在使用分区表的分区方案的分布式数据库里,分区数也是结合上面两点考虑的。 当然分区数太大了,可能会增加分布数据库内部管理成本。分区数量跟分区粒度恰好是相反关系,二者都需要取一个合适的值。 分区数量一旦确定后,调整的成本非常高,通常会引起数据重分布。有些产品可以针对特定类型的分区做分区分裂。如RANGE分区可以分裂为两个RANGE, HASH分区也可以一分为二。只要这个分区分裂的逻辑是数据库内部逻辑实现,保证数据不丢,且对业务透明的,那么风险就很低值得考虑。 分区负载均衡 随着时间的推移,数据库一直在发生各种变化。如QPS增加,数据集更大,或者新增/替换机器等。无论哪种都需要将部分数据分区和相应的请求从一个节点移动到另外一个节点,这个过程称为分区的再平衡(rebalance)。业务对再平衡的要求就是平衡过程中对业务当前读写影响要可控,数据读写服务不能中断。还有一点就是为了再平衡应尽可能少的迁移数据。 前面两个要求都不难满足,最后一个要求就考验各个分区方案的灵活度了。当分区粒度是存储级别的Region时,分区迁移的粒度就是Region,这个对业务也是透明的;分区粒度是分区时,这个取决于各个产品对节点资源管理的设计。比如说有的设计可以做到只需要迁移分区就可以调整各个节点的资源利用率和负载;如果分区方案是分库分表,此时分区粒度是分表。但是数据迁移的单位通常还是实例,利用数据库原生复制能力搭建新的级联备实例,然后新老实例分别删除一半分库数据。这里就迁移了不必要的很多数据分区。 分区访问路由 现在数据分区方案已经确定,业务数据分布在多个节点上。业务应用访问数据库如何连接呢?再分区负载均衡发生后部分分区节点发生变化,业务应用是否要修改连接?这个就是分区访问路由问题,是分布式数据库的基本能力。理论上分区访问路由有三种方案。一是每个节点都可以进行路由转发(如果请求的数据不在该节点上,该节点可以转发应用请求到正确的节点上);二是设置一个中心模块负责接受请求并转发到正确的节点上;三是应用自己获取分布式数据库所有分区的节点信息,直接连接对应的节点,不需要其他组件提供路由功能。 大部分分布式数据库架构,选择了第二种方案,有一个负责分区路由访问的模块。有些产品同时支持这三种方案。 针对分区路由问题情况还可能更复杂。如一个事务有多条SQL时该路由到哪个节点。此外就是如果负责路由的节点故障,或者分区所在节点故障,这个路由不可用或者失效时会如何恢复路由服务。 SQL线性扩展能力 当数据分区方案确定、分区路由问题也解决了后,运维和业务架构为业务的搭建了一个好的分布式数据库环境。很多业务误以为用上分布式数据库后,就一定会很好,或者扩容后业务的性能也能相应的提升。实际使用经验并不一定如此。还是前面那句话使用分区方案主要是获得扩展性,其关键就是分区分布在更多的节点上,能利用上更多节点的能力。 但这个并不是指让单个SQL利用更多节点的能力。举个例子在OLAP业务里,一条SQL 如果能让很多节点同时提供服务,其性能当然是最好的。不过这样的SQL的并发不能太多,否则很容易让所有节点都很忙。即使分布式数据库扩容了节点将分区进一步打散,由于业务的访问压力和数据量也会增加很多,很可能依然是每个SQL同时让所有节点为其服务,这个SQL的吞吐量并不会随着这个节点数量的扩容而得到相应的提升。 分布式数据库的优势在于对于空间问题和请求访问问题分而治之。针对每个分区的访问,由该分区所在的节点响应即可。即使该SQL 并发很高,由于访问的是不同的分区,分别由不同的节点提供服务。每个节点自身也有一定能力满足一定的QPS,所有节点集中在一起就能提供更大的QPS。这个时候如果扩容节点数量,该SQL总的QPS也能获得相应的提升。这是分布式数据库里最好的情形。 第二个例子根据PK 访问表,并且PK还是主键等。通常我们都建议分库分表或者分区时,业务SQL尽量带上拆分键就是这个道理。但是如果业务场景确实无法带上拆分键,除了强制扫描所有分区外,还有个解决方案就是全局索引表。全局索引是独立于数据分区存储的,全局索引可以避免扫描不必要的分区,负面作用就是业务分区的写操作很可能带来分布式事务。 以上两个例子就是分布式数据库里SQL的先行扩展能力的两个极端。前一个场景SQL没有扩展能力,后一个SQL的扩展能力几乎是百分百。大部分SQL的先行扩展能力就界于两者之间。比如说SQL里是分区列的IN条件。这个SQL的先行扩展能力取决于这个INLIST的数据特点。如果恰好每次都是命中同一个分区,那跟分区列等值访问效果一样好;如果INLIST的数据命中绝大部分分区,那就接近OLAP 场景的那个SQL。有些业务增长后,这个INLIST的长度基本不变。比如说人口业务,虽然总人口的激增,但每个家庭的子女数量大部分在1-2。这是一类特点,访问这个子女数据的SQL的先行扩展能力会很好。另外一个例子就是买家订单查询业务。10年前每个买家一段时间的订单数量可能就几个,如今每个买家一段时间的平均订单数量可能在几十或几百。 比INLIST 更复杂的逻辑就是表连接。 表连接时的条件是否是分区列,每个具体的连接值会相应命中多少个分区,是否有分布式执行计划等等。都会影响这个SQL的线性扩展能力。 对于无业务规则的分区方案,虽然分区对业务是透明的,但不可否认的是数据分区是分布在不同的节点上,只要业务读写这些数据,数据分布特点就会影响到SQL的性能。对于业务而言,该如何选择?如果业务通过分区策略控制数据分区分布特点,能够获得更高的性能,业务是否愿意选择会影响分布式数据库的选型。而不同分区方案在运维方面的特点也不一样,是影响选型的另外一个因素,这里就不细说。 蚂蚁的分布式数据库最佳实践 蚂蚁金服的业务规模非常大,业务模块划分非常细。以网商银行非常核心的交易、账务和支付模块举例,每个业务模块的数据经分布式数据库中间件(SOFA的DBP)拆分为多个OceanBase租户(实例)下百库百表,每个表同时变更为OceanBase自身的分区表,分为100个分区。总共有多个OceanBase集群,每个集群横跨杭州上海和深圳五机房,并同时提供服务。这里的数据总共分为10000个分区,不同分库下的数据分区的Leader副本分别位于不同的机房。不同分表之间可以分别进行结构变更(灰度发布能力),不同OceanBase租户甚至集群之间是物理隔离的,这是金融核心业务拆分有使用分库分表的第一个原因。 业务层面数据是按用户维度拆分的,不同的用户访问不同的机房的应用和数据。业务层面的流量分配规则和数据分区Leader副本分配规则保持一致并联动,实现了任意时刻的在线业务流量机房间比例调整。这是拆分使用分库分表的第二个原因。 OceanBase集群在蚂蚁金服业务里的核心作用是在数据库层面解决数据副本三地分布的强一致和高可用切换问题,并且提供了在线分区迁移和租户弹性伸缩能力。 后记 本文首先针对分布式数据库种的数据存在的两种形式副本(复制产生的)和分区(分区产生的)进行区分。然后总结了分区方案需要考虑的几个点:分区粒度、分区策略、分区迁移和负载均衡、分区数量和分区路由问题等。即使这些都考虑好了,也只是分布数据库这个初局做好了。后面业务能否发挥分布式数据库的优势就取决于业务SQL的写法是否有很好的线性扩展能力。最后简单总结了蚂蚁金服支付宝和网上银行在分布式数据库架构方面的最佳实践。 推荐阅读 分布式数据库的拆分设计实践 说说数据库事务和开发(下)—— 分布式事务 如何基于OceanBase构建应用和数据库的异地多活 揭秘OceanBase的弹性伸缩和负载均衡原理 网商银行 × OceanBase:首家云上银行的分布式数据库应用实践 [Designing Data-Intensive Applications(文中配图大部分来自本书)]() 作者:mq4096 原文链接 本文为云栖社区原创内容,未经允许不得转载。
「深度学习福利」大神带你进阶工程师,立即查看>>> 作者:lan 本文为 DM 源码阅读系列文章的第八篇, 上篇文章 对 DM 中的定制化数据同步功能进行详细的讲解,包括库表路由(Table routing)、黑白名单(Black & white table lists)、列值转化(Column mapping)、binlog 过滤(Binlog event filter)四个主要功能的实现。 本篇文章将会以 gh-ost 为例,详细地介绍 DM 是如何支持一些 MySQL 上的第三方 online schema change 方案同步,内容包括 online schema change 方案的简单介绍,online schema change 同步方案,以及同步实现细节。 MySQL 的 Online Schema Change 方案 目前有一些第三方工具支持在 MySQL 上面进行 Online Schema Change,比较主流的包括 pt-online-schema-change 和 gh-ost 。 这些工具的实现原理比较类似,本文会以 gh-ost 为例来进行分析讲解。 从上图可以大致了解到 gh-ost 的逻辑处理流程: 在操作目标数据库上使用 create table ghost table like origin table 来创建 ghost 表; 按照需求变更表结构,比如 add column/index ; gh-ost 自身变为 MySQL replica slave,将原表的全量数据和 binlog 增量变更数据同步到 ghost 表; 数据同步完成之后执行 rename origin table to table_del, table_gho to origin table 完成 ghost 表和原始表的切换 pt-online-schema-change 通过 trigger 的方式来实现数据同步,剩余流程类似。 在 DM 的 task 配置中可以通过设置 online-ddl-scheme 来配置的 online schema change 方案,目前仅支持 gh-ost/pt 两个配置选项。 DM Online Schema Change 同步方案 根据上个章节介绍的流程,pt 和 gh-ost 除了 replicate 数据的方式不一样之外,其他流程都类似,并且这种 native 的模式可以使得 binlog replication 几乎不需要修改就可以同步数据。但是 DM 为了减少同步的数据量,简化一些场景(如 shard tables merge)下的处理流程,并做了额外的优化,即,不同步 ghost 表的数据。 继续分析 online schema change 的流程,从数据同步的角度看有下面这些需要关注的点: 原始表的增量数据同步模式有没有变化 ghost 表会产生跟原始表几乎一样的冗余 binlog events 通过 rename origin table to table_del, table_gho to origin table 完成 ghost 表和原始表的切换 如果使用 ghost 表的 alter DDL 替换掉 rename origin table to table_del, table_gho to origin table ,那么就可以实现我们的不同步 ghost 表数据的目的。 DM Online Schema Change 同步实现细节 Online schema change 模块代码实现如下: gh-ost 同步代码实现 pt-online-schema-change 同步代码实现 DM 将 同步的表分为三类 : real table - 原始表 trash table - online schema change 过程中产生的非关键数据表,比如以 _ghc , _del 为后缀的表 ghost table - 与原始表对应的经过 DDL 变更的数据表,比如以 _gho 为后缀的表 当 DM 遇到 DDL 的时候,都会 调用 online schema change 模块的代码进行处理 ,首先判断表的类型,接着针对不同类型作出不同的处理: real table - 对 rename table statement 进行模式检查,直接返回执行 trash table - 对 rename table statement 做一些模式检查,直接忽略同步 ghost table 如果 DDL 是 create/drop table statement ,则 清空内存中的残余信息后忽略这个 DDL 继续同步 如果 DDL 是 rename table statement ,则 返回内存中保存的 ghost table 的 DDLs 如果是其他类型 DDL, 则把这些 DDL 保存在内存中 下面是一个执行示例,方便大家对照着来理解上面的代码逻辑: Section 1: 使用 create table like statement 创建 ghost table,DM 会清空内存中 online_ddl . _t2_gho 对应的 DDL 信息 Section 2: 执行 alter table statement,DM 会保存 DDL 到内存中 Section 3:trash table 的 DDLs 会被忽略 Section 4:遇到 ghost table 的 rename table statement 会替换成 Section 2 的 DDL, 并且将该 DDL 的 table name 更换成对应 real table name 去执行 注意: rename table statement 模式检查主要是为了确保在 online schema change 变更过程中除了 rename origin table to table_del, table_gho to origin table 之外没有其他 rename table statement,避免同步状态的复杂化。 小结 本篇文章详细地介绍 DM 对 online schema change 方案的同步支持,内容包含 online schema change 方案的简单介绍, online schema change 同步方案,以及同步实现细节。下一章会对 DM 的 shard DDL merge 方案进行详细的讲解,敬请期待。 原文阅读 : https://www.pingcap.com/blog-cn/dm-source-code-reading-8/
「深度学习福利」大神带你进阶工程师,立即查看>>> 2019年6月14日,由广州市科学技术局指导、广州市科技创新企业协会、《快公司FastCompany》联合主办的2018年独角兽创新企业颁牌暨2019广州“发现独角兽”创新企业项目启动仪式在广东迎宾馆顺利举办。 巨杉数据库再获“独角兽”企业称号。 2018年12月,广州市科创企业协会公示了 “独角兽”创新企业榜单遴选结果。榜单涵盖40家企业,覆盖大数据、云计算、信息技术、物联网、生物医药、人工智能、智能硬件、新能源、新材料、先进制造等战略性新兴产业。云从科技、极飞科技、亿航智能、巨杉数据库等创新型独角兽企业跃然在榜。 作为领先的新一代分布式数据库厂商,巨杉数据库一直坚持产品的自研,在过去一年中,技术和产品保持创新,相继发布了3.0和3.2版本,全面增强OLTP业务性能。在市场端,巨杉数据库再接再厉,在金融标杆行业已经逐渐建立壁垒,同时在金融核心业务应用场景上不断增强。可以说,巨杉在科技创新独角兽的道路上,步伐越来越扎实。 关于巨杉: 巨杉数据库专注新一代分布式数据库技术研发,自2011年成立以来,坚持从零开始打造分布式开源数据库引擎,是中国首家连续两年入选 Gartner 数据库报告的数据库厂商。 巨杉数据库的主要产品包括 SequoiaDB 分布式关系型数据库与 SequoiaCM 企业内容管理软件,应用场景包括分布式在线交易、数据中台、分布式内容管理等。 目前,巨杉数据库已在超过50家500强级别的大型商业银行核心生产业务上线,企业用户总数超过1000家。
「深度学习福利」大神带你进阶工程师,立即查看>>> Mysql存储过程查询结果赋值到变量的方法 把查询结果赋值到变量,大部分情况下使用游标来完成,但是如果明确知道查询结果只有一行(例如统计记录的数量,某个字段求和等),其实可以使用set或into的方式来实现赋值。示例代码: drop table if exists test_tbl; create table test_tbl (name varchar(20), status int(2)); insert into test_tbl values('abc', 1),('edf', 2),('xyz', 3); drop procedure IF EXISTS pro_test_3; delimiter // create procedure pro_test_3() begin -- 方式 1 DECLARE cnt INT DEFAULT 0; select count(*) into cnt from test_tbl; select cnt; -- 方式 2 set @cnt = (select count(*) from test_tbl); select @cnt; -- 方式 3 select count(*) into @cnt1 from test_tbl; select @cnt1; -- 多个列的情况下似乎只能用 into 方式 select max(status), avg(status) into @max, @avg from test_tbl; select @max, @avg; end delimiter ; call pro_test_3(); 简单的存储过程各个关键字的用法: CREATE DEFINER = CURRENT_USER PROCEDURE `NewProc`(in _xnb varchar(50)) BEGIN ## 定义变量 DECLARE _num FLOAT(14,6) DEFAULT 0; ## @表示全局变量 相当于php $ ## 拼接赋值 INTO 必须要用全局变量不然语句会报错 ## //CONCAT会把'SELECT SUM('和_xnb和') INTO @tnum FROM btc_user_coin'拼接起来,CONCAT的各个参数中间以","号分割 SET @strsql = CONCAT('SELECT SUM(',_xnb,') INTO @tnum FROM btc_user_coin'); ## 预处理需要执行的动态SQL,其中stmt是一个变量 PREPARE stmt FROM @strsql; ## 执行SQL语句 EXECUTE stmt; ## 释放掉预处理段 deallocate prepare stmt; ## 赋值给定义的变量 SET _num = @tnum; SELECT _num END;; mysql 存储过程中使用动态sql语句 Mysql 5.0 以后,支持了动态sql语句,我们可以通过传递不同的参数得到我们想要的值 这里介绍两种在存储过程中的动态sql 1. set sql = (预处理的sql语句,可以是用concat拼接的语句) set @sql = sql PREPARE stmt _name FROM @sql ; EXECUTE stmt _name ; {DEALLOCATE | DROP} PREPARE stmt _name ; 过程过程示例: CREATE DEFINER = `root`@`%` PROCEDURE `NewProc`(IN `USER_ID` varchar(36),IN `USER_NAME` varchar(36)) BEGIN declare SQL_FOR_SELECT varchar(500); -- 定义预处理sql语句 set SQL_FOR_SELECT = CONCAT("select * from user where user_id = '",USER_ID,"' and user_name = '",USER_NAME,"'"); -- 拼接查询sql语句 set @sql = SQL_FOR_SELECT; PREPARE stmt FROM @sql; -- 预处理动态sql语句 EXECUTE stmt ; -- 执行sql语句 deallocate prepare stmt; -- 释放prepare END; 上述是一个简单的查询用户表的存储过程,当我们调用此存储过程,可以根据传入不同的参数获得不同的值。 但是:上述存储过程中,我们必须在拼接sql语句之前把USER_ID,USER_NAME定义好,而且在拼接sql语句之后,我们无法改变USER_ID,USER_NAME的值,如下: CREATE DEFINER = `root`@`%` PROCEDURE `NewProc`(IN `USER_ID` varchar(36),IN `USER_NAME` varchar(36)) BEGIN declare SQL_FOR_SELECT varchar(500); -- 定义预处理sql语句 set SQL_FOR_SELECT = CONCAT("select * from user where user_id = '",USER_ID,"' and user_name = '",USER_NAME,"'"); -- 拼接查询sql语句 set @sql = SQL_FOR_SELECT; PREPARE stmt FROM @sql; -- 预处理动态sql语句 EXECUTE stmt ; -- 执行sql语句 deallocate prepare stmt; -- 释放prepare set USER_ID = '2'; -- 主动指定参数USER_ID的值 set USER_NAME = 'lisi'; set @sql = SQL_FOR_SELECT; PREPARE stmt FROM @sql; -- 预处理动态sql语句 EXECUTE stmt ; -- 执行sql语句 deallocate prepare stmt; -- 释放prepare END; 我们用call aa('1','zhangsan');来调用该存储过程,第一次动态执行,我们得到了‘张三’的信息,然后我们在第14,15行将USER_ID,USER_NAME改为lisi,我们希望得到李四的相关信息,可查出来的结果依旧是张三的信息,说明我们在拼接sql语句后,不能再改变参数了。 为了解决这种问题,下面介绍第二中方式: 2. set sql = (预处理的sql语句,可以是用concat拼接的语句,参数用 ?代替) set @sql = sql PREPARE stmt _name FROM @sql ; set @ var_name = xxx; EXECUTE stmt _name USING [USING @ var_name [, @ var_name ] ...] ; {DEALLOCATE | DROP} PREPARE stmt _name ; 上述的代码我们就可以改成 : CREATE DEFINER = `root`@`%` PROCEDURE `NewProc`(IN `USER_ID` varchar(36),IN `USER_NAME` varchar(36)) BEGIN declare SQL_FOR_SELECT varchar(500); -- 定义预处理sql语句 set SQL_FOR_SELECT = "select * from user where user_id = ? and user_name = ? "; -- 拼接查询sql语句 set @sql = SQL_FOR_SELECT; PREPARE stmt FROM @sql; -- 预处理动态sql语句 set @parm1 = USER_ID; -- 传递sql动态参数 set @parm2 = USER_NAME; EXECUTE stmt USING @parm1 , @parm2; -- 执行sql语句 deallocate prepare stmt; -- 释放prepare set @sql = SQL_FOR_SELECT; PREPARE stmt FROM @sql; -- 预处理动态sql语句 set @parm1 = '2'; -- 传递sql动态参数 set @parm2 = 'lisi'; EXECUTE stmt USING @parm1 , @parm2; -- 执行sql语句 deallocate prepare stmt; -- 释放prepare END; 这样,我们就可以真正的使用不同的参数(当然也可以在存储过程中通过逻辑生成不同的参数)来使用动态sql了。 几个注意: 存储动态SQL的值的变量不能是自定义变量,必须是用户变量或者全局变量 如:set sql = 'xxx'; prepare stmt from sql;是错的,正确为: set @sql = 'xxx'; prepare stmt from @sql; 即使 preparable_stmt 语句中的 ? 所代表的是一个字符串,你也不需要将 ? 用引号包含起来。 如果动态语句中用到了 in ,正常写法应该这样:select * from table_name t where t.field1 in (1,2,3,4,...); 则sql语句应该这样写:set @sql = "select * from user where user_id in (?,?,?) " 因为有可能我不确定in语句里有几个参数,所以我试过这么写 set @sql = "select * from user where user_id in (?) " 然后参数我传的是 "'1','2','3'" 我以为程序会将我的动态sql解析出来(select * from user where user_id in ('1','2','3')) 但是并没有解析出来,在写存储过程in里面的列表用个传入参数代入的时候,就需要用到如下方式: 1.使用find_in_set函数 select * from table_name t where find_in_set(t.field1,'1,2,3,4'); 2.还可以比较笨实的方法,就是组装字符串,然后执行 DROP PROCEDURE IF EXISTS photography.Proc_Test; CREATE PROCEDURE photography.`Proc_Test`(param1 varchar(1000)) BEGIN set @id = param1; set @sel = 'select * from access_record t where t.ID in ('; set @sel_2 = ')'; set @sentence = concat(@sel,@id,@sel_2); -- 连接字符串生成要执行的SQL语句 prepare stmt from @sentence; -- 预编释一下。 “stmt”预编释变量的名称, execute stmt; -- 执行SQL语句 deallocate prepare stmt; -- 释放资源 END; 存储过程(无参,IN多个输入参数,OUT多个输出参数,INOUT输入输出) https://blog.csdn.net/www1056481167/article/details/83343341 存储过程: 1、减少编译次数 2、简化操作 3、减少了变异次数减少了和数据库的链接次数,提高效率 关于存储过程的方法: 1、删除存储过程 DROP PROCEDURE 存储过程的名称; 2、查看存储过程信息 show create procedure 存储过程名称; 3、没有存储过程的修改 创建语法: CREATE PROCEDURE 存储过程名称(参数列表) BEGIN 方法体(存储过程体)一组方法语句; END ⭐⭐⭐⭐注意:参数列表包括三部分 1、参数模式 参数名 参数类型 eg: IN stuname varchar(20) 参数模式 IN : 该参数可以作为输入,也就是需要调用方传入值 OUT: 该参数作为输出,也就是该参数可以作为返回值 INOUT: 既可以作为输入参数,也可以作为输出参数 2、如果存储过程提仅仅只有一句话,那么BEGIN END 可以省略 3、存储过程中的每一条sql语句的结尾都必须加封号,需要申明结尾符号 DELIMITER 重新设置 以下在创建的过程中使用DELIMITER $开始,DELIMITER ;结束,是因为mysql是以;结束的,此处创建存储过程的时候需要指定结尾符号为$,整个存储过程执行完成后,再还原为mysql的结束符;即可 一、空参存储过程 DELIMITER $ CREATE PROCEDURE test1() BEGIN INSERT INTO admin(username,`password`) VALUES ('JOHN1','000001'); INSERT INTO admin(username,`password`) VALUES ('JOHN2','000002'); INSERT INTO admin(username,`password`) VALUES ('JOHN3','000003'); INSERT INTO admin(username,`password`) VALUES ('JOHN4','000004'); INSERT INTO admin(username,`password`) VALUES ('JOHN5','000005'); END $ 调用:CALL test1(); 二、入参存储过程(此处直接举例有多个入参的存储过程的写法) # 示例,查询是否登陆成功 DELIMITER $ CREATE PROCEDURE testParams(IN username varchar(20),IN password varchar(20)) BEGIN DECLARE RESULT VARCHAR(20) DEFAULT '';#申明并初始化 select COUNT(1) INTO RESULT #赋值将统计到的count(1) 赋值给RESULT结果 from admin where admin.username=username #如果参数名相同的话指明参数是那个表的字段即可 and admin.password=password; END $ DELIMITER ; 调用:call testParams('11','22'); 三、多个出参的存储过程(此处直接举例有多个出餐的存储过程的写法) #根据女神名,查询男神名和魅力值 DELIMITER $ CREATE PROCEDURE myp7(IN beautyName varchar(20),OUT boyName VARCHAR(20),OUT userCP varchar(20)) BEGIN SELECT bo.boyName,bo.userCP INTO boyName,userCP #此处赋值的时候多个直接INTO为对应位置的出参对象即可 FROM boys bo INNER JOIN beauty b on bo.id=b.boyfriend_id WHERE b.name=beautyName; END $ DELIMITER ; 调用:CALL myp7('小昭',@boyName,@userCP); #此处存储过程的两个入参(也即出参)用@符号定义即可,也可以先定义好传入到里面 SELECT @boyName AS boyName,@userCP as userCP #查询存储过程运行的结果 四、创建带有INOUT的存储过程 #案例 传入a,b 最终a,b翻倍,并返回 DELIMITER $ CREATE PROCEDURE ccgc(INOUT a int,INOUT b int) BEGIN SET a=a*2; SET b=b*2; END $ DELIMITER ; 调用: ###此处需要提前定义参数值,类似于java的入参, SET @m=10; SET @n=30; call ccgc(@m,@n); #然后调用,并打印输出 select @m,@n ---------------------
「深度学习福利」大神带你进阶工程师,立即查看>>> oracheck来源于Oracle为RAC环境准备的工具raccheck。 oracheck是需要单独从MOS上下载的工具包。和其他Oracle产品不同,oracheck更新速度非常快,官方建议是90天就需要下载最新的oracheck版本。 这样做的目的就是让用户可以获得最新的检查项目和建议,及时下载安装最新的安全补丁。 贴上下载地址: https://support.oracle.com/epmos ... l-state=7zuzpudjx_4 下面简单介绍一下,oracheck工具的使用 (1)上传包到服务器 [root@rac1 oracheck]# ll total 257500 -rw-r--r--. 1 root root 263673609 Jan 25 15:33 orachk.zip (2)解压 [root@rac1 oracheck]# unzip orachk.zip 解压过程略 (3)将目录授权给oracle用户 [root@rac1 oracle]# chown -R oracle:dba oracheck/ [root@rac1 oracle]# su - oracle cd [oracle@rac1 ~]$ cd /oracle/oracheck/ [oracle@rac1 oracheck]$ ll total 325368 -r--r-----. 1 oracle dba 8233480 Dec 13 01:42 Apex5_CollectionManager_App.sql drwxr-x---. 2 oracle dba 4096 Dec 13 01:41 bash drwxr-x---. 3 oracle dba 4096 Jan 25 15:37 build -r--r-----. 1 oracle dba 2958609 Dec 13 01:42 CollectionManager_App.sql -r--r-----. 1 oracle dba 49352682 Dec 13 01:03 collections.dat drwxr-xr-x. 2 oracle dba 4096 Jan 25 15:36 doc drwxr-x---. 2 oracle dba 4096 Dec 13 05:31 exadiscover drwxr-x---. 3 oracle dba 4096 Dec 13 05:31 lib -rwxr-x---. 1 oracle dba 11063 Dec 13 05:31 orachk -r-xr-x---. 1 oracle dba 5665 Dec 13 01:41 orachk.bat -rw-r--r--. 1 oracle dba 2296616 Dec 13 05:45 ORAchk_Health_Check_Catalog.html -r--r-----. 1 oracle dba 109317 Dec 13 01:41 orachk.pyc -rw-r--r--. 1 oracle dba 263673609 Jan 25 15:33 orachk.zip -rw-r--r--. 1 oracle dba 10105 Dec 13 05:45 readme.txt -r--r-----. 1 oracle dba 6424905 Dec 13 01:03 rules.dat -r--r-----. 1 oracle dba 40052 Dec 13 01:42 sample_user_defined_checks.xml drwxr-x---. 2 oracle dba 4096 Dec 13 05:31 templates -r--r-----. 1 oracle dba 2888 Dec 13 01:42 user_defined_checks.xsd -rw-r--r--. 1 oracle dba 230 Dec 13 05:45 UserGuide.txt orachk工具有很多参数,可通过-h选项查看 [oracle@rac1 oracheck]$ ./orachk -h | -S | -a | -acchk | -allserial | -appjar | -apptrc | -asmhome | -auto_restart | -b | -baseline | -c | -cells | -cellserial | -celltmpdir ................. ................. (4)运行orachk工具对服务器进行检查 ps:如果是rac环境,那么在运行过程中,也会对其他节点进行检查,因此只在一个节点运行即可。 [oracle@rac1 oracheck]$ ./orachk CRS stack is running and CRS_HOME is not set. Do you want to set CRS_HOME to /oracle/app/11.2.0/grid?[y/n][y] y Checking ssh user equivalency settings on all nodes in cluster for oracle Node rac2 is configured for ssh user equivalency for oracle user Searching for running databases . . . . . . . List of running databases registered in OCR 1. orcl 2. None of above Select databases from list for checking best practices. For multiple databases, select 1 for All or comma separated number like 1,2 etc [1-2][1]. 1 . . . . Checking Status of Oracle Software Stack - Clusterware, ASM, RDBMS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Oracle Stack Status Host Name CRS Installed RDBMS Installed CRS UP ASM UP RDBMS UP DB Instance Name rac1 Yes Yes Yes Yes Yes orcl1 rac2 Yes Yes Yes Yes No ............. ............. ............. Clusterware patches summary report Total patches Applied on CRS Applied on RDBMS Applied on ASM 1 0 0 0 RDBMS homes patches summary report Total patches Applied on RDBMS Applied on ASM ORACLE_HOME 1 2 0 /oracle/app/oracle/product/11.2.0/db CLUSTERWIDE CHECKS Detailed report (html) - /oracle/oracheck/orachk_rac1_orcl_012518_163134/orachk_rac1_orcl_012518_163134.html UPLOAD [if required] - /oracle/oracheck/orachk_rac1_orcl_012518_163134.zip
「深度学习福利」大神带你进阶工程师,立即查看>>> 十多年来,NAS中已经存在的目录和文件达到10亿之多,在设计和开发备份系统的过程中碰到了很多挑战,本文将分享大量文件名记录的树形结构存储实践。 一、引言 既然是定期备份,肯定会有1次以上的备份。对于一个特定目录,每次备份时都要与上次备份时进行比较,以期找出哪些文件被删除了,又新增了哪些文件,这就需要每次备份时把该目录下的所有文件名进行保存。我们首先想到的是把所有文件名用特定字符进行拼接后保存。由于我们使用了MySQL保存这些信息,当目录下文件很多时,这种拼接的方式很可能超出MySQL的Blob长度限制。根据经验,当一个目录有大量文件时,这些文件的名称往往是程序生成的,有一定规律的,而且开头一般是重复的,于是我们想到了使用一种树形结构来进行存储。 例如,一个有abc、abc1、ad、cde 4个文件的目录对应的树如图1所示。 图1 树形结构示例 图1中,R表示根节点,青色节点我们称为结束节点,从R到每个结束节点的路径都表示一个文件名。可以在树中查找是否含有某个文件名、遍历树中所有的文件名、对树序列化进行保存、由序列化结果反序列化重新生成树。 二、涉及的数据结构 注意:我们使用java编写,文中涉及语言特性相关的知识点都是指java。 2.1 Node的结构 包括根节点在内的每个节点都使用Node类来表示。代码如下: class Node { private char value; private Node[]children = new Node[0]; private byte end = 0; } 字段说明: value:该节点表示的字符,当Node表示根节点时,value无值。 children:该节点的所有子节点,初始化为长度为0的数组。 end:标记节点是否是结束节点。0不是;1是。叶子节点肯定是结束节点。默认非结束节点。 2.2 Node的操作 public Node(char v); public Node findChild(char v); public Node addChild(char v); 操作说明: Node:构造方法。将参数v赋值给this.value。 findChild:查找children中是否含有value为v的子节点。有则返回子节点,没有则返回null。 addChild:首先查找children中是否已经含有value为v的子节点,如果有则直接将查到的子节点返回;否则创建value为v的节点,将children的长度延长1,将新创建的节点作为children的最后一个元素,并返回新创建的节点。 2.3 Tree的结构 class Tree { public Node root = new Node(); } 字段说明:Tree只含有root Node。如前所述,root的value无值,end为0。初始时的children长度为0。 2.4 Tree的操作 public void addName(String name) ; public boolean contain(String name); public Found next(Found found); public void writeTo(OutputStream out); public static Tree readFrom(InputStream in); 操作说明: addName:向树中增加一个新的文件名,即参数name。以root为起点,name中的每个字符作参数调用addChild,返回值又作为新的起点,直到name中的全部字符添加完毕,对最后一次调用addChild的返回值标记为结束节点。 contain:查询树中是否含有一个文件名。 next:对树中包含的所有文件名进行遍历,为了使遍历能够顺利进行,我们引入了新的类Found,细节会在后文详述。 writeTo:将树写入一个输出流以进行持久化。 readFrom:此方法是静态方法。从一个输入流来重新构建树。 三、树的构建 在新建的Tree上调用addName方法,将所有文件名添加到树中,树构建完成。仍然以含有abc、abc1、ad、cde 四个文件的目录为例,对树的构建进行图示。 图2 树的构建过程 图2中,橙色节点表示需要在该节点上调用addChild方法增加子节点,同时addChild的返回值作为新的橙色节点。直到没有子节点需要增加时,把最后的橙色节点标记为结束节点。 四、树的查询 查找树中是否含有一个某个文件名,对应Tree的contain方法。在图2中的结果上分别查找ef、ab和abc三个文件来演示查找的过程。如图3所示。 图3 树的查询示意图 图3中,橙色节点表示需要在该节点上调用findChild方法查找子节点。 五、树的遍历 此处的遍历不同于一般树的遍历。一般遍历是遍历树中的节点,而此处的遍历是遍历根节点到所有结束节点的路径。 我们采用从左到右、由浅及深的顺序进行遍历。我们引入了Found类,并作为next方法的参数进行遍历。 5.1 Found的结构 class Found { private String name; private int[] idx ; } 为了更加容易的说明问题,在图1基础上进行了小小的改造,每个节点的右下角增加了下标,如图4。 图4 带下标的Tree 对于abc这个文件名,Found中的name值为“abc”,idx为{0,0,0}。 对于abc1这个文件名,Found中的name值为“abc1”,idx为{0,0,0,0}。 对于ad这个文件名,Found中的name值为“ad”,idx为{0,1}。 对于cde这个文件名,Found中的name值为“cde”,idx为{1,0,0}。 5.2 如何遍历 对于图4而言,第一次调用next方法应传入null,则返回第一个结果,即abc代表的Found;继续以这个Found作为参数进行第二次next的调用,则返回第二个结果,即abc1代表的Found;再继续以这个Found作为参数进行第三次next的调用,则返回第三个结果,即ad所代表的Found;再继续以这个Found作为参数进行第四次next的调用,则返回第四个结果,即cde所代表的Found;再继续以这个Found作为参数进行第五次调用,则返回null,遍历结束。 六、序列化与反序列化 6.1 序列化 首先应该明确每个节点序列化后应该包含3个信息:节点的value、节点的children数量和节点是否为结束节点。 6.1.1 节点的value 虽然之前所举的例子中节点的value都是英文字符,但实际上文件名中可能含有汉字或者其他语言的字符。为了方便处理,我们没有使用变长编码。而是直接使用unicode码。字节序采用大端编码。 6.1.2 节点的children数量 由于节点的value使用了unicode码,所以children的数量不会多于unicode能表示的字符的数量,即65536。children数量使用2个字节。字节序同样采用大端编码。 6.1.3 节点的end 0或1可以使用1位(1bit)来表示,但java中最小单位是字节。如果采用1个字节来表示end,有些浪费空间,其实任何一个节点children数量达到65536/2的可能性都是极小的,因此我们考虑借用children数量的最高位来表示end。 综上所述,一个节点序列化后占用4个字节,以图4中的根节点、value为b的节点和value为e的节点为例: 表1 Node序列化示例 value的unicode children数量 end children数量/(end<<15) 最终结果 根节点
b节点 e节点 0x0000 0x0062 0x0065 2 1 0 0 0 1 0x0002 0x0001 0x8000 0x00020000 0x00010062 0x80000065 6.1.4 树的序列化过程 对树进行广度遍历,在遍历过程中需要借助队列,以图4的序列化为例进行说明: 图5 对图4的序列化过程 6.2 反序列化 反序列化是序列化的逆过程,由于篇幅原因不再进行阐述。值得一提的是,反序列化过程同样需要队列的协助。 七、讨论 7.1 关于节省空间 为方便讨论,假设目录下的文件名是10个阿拉伯数字的全排列,当位数为1时,目录下含有10个文件,即0、1、2……8、9,当位数为2时,目录下含有100个文件,即00、01、02……97、98、99,以此类推。 比较2种方法,一种使用“/”分隔,另一种是本文介绍的方法。 表2 2种方法的存储空间比较(单位:字节) 位数 方法 1 2 3 4 5 6 “/”分隔 Tree 19 44 299 444 3999 4444 49999 44444 599999 444444 6999999 4444444 由表2可见,当位数为4时,使用Tree的方式开始节省空间,位数越多节省的比例越高,这正是我们所需要的。 表中,使用“/”分隔时,字节数占用是按照utf8编码计算的。如果直接使用unicode进行存储,占用空间会加倍,那么会在位数为2时就开始节省空间。同样使用“/”分隔,看起来utf8比使用unicode会更省空间,但实际上,文件名中有时候会含有汉字,汉字的utf8编码占用3个字节。 7.2 关于时间 在树的构建、序列化反序列化过程中,引入了额外的运算,根据我们的实践,user CPU并没有明显变化。 7.3 关于理想化假设 最初我们就是使用了“/”分隔的方法对文件名进行存储,并且数据库的相应字段类型是Blob(Blob的最大值是65K)。在测试阶段就发现,超出65K是一件很平常的事情。在不可能预先统计最大目录里所有文件名拼接后的大小的情况下,我们采取了2种手段,一是使用LongBlob类型,另一种就是尽量减小拼接结果的大小,即本文介绍的方法。 即使使用树形结构来存储文件名,也不能够保证最终结果不超出4G(LongBlob类型的最大值),至少在我们实践的过程并未出现问题,如果真出现这种情况,只能做特殊处理了。 7.4 关于其他压缩方法 把文件名使用“/”拼接后,使用gzip等压缩算法对拼接结果进行压缩后再存储,在节省存储空间方面会取得更好的效果。但是在压缩之前,拼接结果存在于内存,这样对JVM的堆内存有比较高的要求;另外,使用“/”拼接时,查找会比较麻烦。 作者:牛宁昌 来源:宜信技术学院 「深度学习福利」大神带你进阶工程师,立即查看>>> MongoDB必须注意信息 不要使用Kill -9 在关闭进程时, 可以使用pkill killall 但是不能使用kill -9 一旦使用该命令, 可能导致mongo再也起不来了,造成锁文件. 如果不小心使用了Kill -9 到Mongo的data目录下放置数据库文件的地方, 也有可能在其他路径. 查找mongod.lock的锁文件. 删除即可. 错误问题收集 启动错误 今天在Ubuntu中启动mongo /usr/bin/mongod --dbpath=data --logpath=log/mongo.log --fork 发生错误: ERROR: child process failed, exited with error number 100 虽然子进程启动失败了, 但是日志正常输出了. 通过查看./log/mongo.log的错误内容 发现一行ERROR: mongo ERROR: Insufficient free space for journal files 解决错误: 在启动命令加上--smallfiles /usr/bin/mongod --dbpath=data --logpath=log/mongo.log --smallfiles --fork 即使用,使用较小的默认文件大小. 或修改配置文件 smallfiles = true 原因: 因为journal至少以2G的数量进行增长,当磁盘空间不足时,会报错Insufficient free space for journal files. 意外停止导致的启动错误 比如非法关闭的时候第二次启动mongo.lock文件报lock is not empty错.原因是MongoDB的文件锁防重复启动机制 主要通过删除锁文件, 使用--repair 启动参数修复. --repair参数官方解释 通过丢弃无效或损坏的数据重新构建数据库和索引,这些数据可能由于意外的系统重新启动或崩溃而出现。 案例: rm /安装目录/mongodb/mongo.lock && ./mongod --repair
「深度学习福利」大神带你进阶工程师,立即查看>>> 前言 大部分开发者都知道加索引会快。但实际过程中,我们常碰到一些疑问&困难: 我们查询的字段会各种case都有,是不是各个涉及查询的字段都要加索引? 复合索引和单字段怎么选择,都加还是每一个的单个字段就好了? 加索引有没有副作用? 索引都加了,但还是不够快?怎么办? 本文尝试去解释索引的基本知识&解答上述的疑问。 一、索引究竟是什么东西? 大部分开发者接触索引,大概知道索引类似书的目录,你要找到想要的内容,通过目录找到限定的关键字,进而找到对应的章节的pageno,再找到具体的内容。 在数据结构里面,最简单的索引实现类似hashmap,通过关键字key,映射到具体的位置,找到具体的内容。但除了hash的方式,还有多种的方式实现索引。 (一)索引的多种实现方式以及特色 hash / b-tree / b+-tree redis HSET / MongoDB&PostgreSQL / MySQL hashmap 一图见b-tree & b+-tree 差别: b+-tree 叶子存数据,非叶子存索引,不存数据,叶子间有link b-tree 非叶子可存数据 算法查找复杂度上来说: hash 接近O(1) b-tree O(1)~ O(Log(n))更快的平均查找时间,不稳定的查询时间 b+ tree O(Log(n)) 连续数据, 查询的稳定性 至于为何MongoDB 的实现选择b-tree 而非 b+-tree ? 网上多篇文章有阐述,非本文重点。 (二)数据&索引的存储 index尽量存储在内存,data 其次。 注意只保留必要的index,内存尽量用在刀刃上。 如果index memory 都接近占满memory,那么就很容易读到disk,速度就下来了。 (三)知道索引的实现&存储原理后的思考 insert/update/delete 会触发rebalance tree,所以,增删改数据,索引会触发修改,性能会有损耗,索引不是越多越好。既然如此,选哪些字段作为索引呢?当查询用到这些条件,怎么办? 拿一个最简单的hashmap来讲,为什么复杂度不是O(1),而是所谓接近 O(1)。因为有key 冲突/重复,DB 去找的时候,key 冲突的数据一大堆的话,还是得轮着继续找。b-tree 看键(key)的选择也是如此。 因此一个大部分开发经常犯的错就是对没有区分度的key建索引。例如:很多就只有集中类别的 type/status 的 documents count 达几十万以上的collection,通常这种索引没什么帮助。 二、复合索引 (一)复合索引不是越多越好 如果不想多建多余的索引,开发的同事在复合 & 单个字段选择上有时候挺纠结的。 根据典型碰到的场景,来做几个实验: 这里创建了个loans collection。简化只有100条数据。这个是借贷的表有 _id, userId, status(借贷状态), amount(金额). db.loans.count()100 db.loans.find({ "userId" : "59e022d33f239800129c61c7", "status" : "repayed", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "status" : { "$eq" : "repayed" } }, { "userId" : { "$eq" : "59e022d33f239800129c61c7" } } ] }, "queryHash" : "15D5A9A1", "planCacheKey" : "15D5A9A1", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "$and" : [ { "status" : { "$eq" : "repayed" } }, { "userId" : { "$eq" : "59e022d33f239800129c61c7" } } ] }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 注意上面 COLLSCAN 全表扫描了,因为没有索引。接下来我们分别建立几个索引。 step 1 先建立 {userId:1, status:1} db.loans.createIndex({userId:1, status:1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } db.loans.find({ "userId" : "59e022d33f239800129c61c7", "status" : "repayed", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "status" : { "$eq" : "repayed" } }, { "userId" : { "$eq" : "59e022d33f239800129c61c7" } } ] }, "queryHash" : "15D5A9A1", "planCacheKey" : "BB87F2BA", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ], "status" : [ "["repayed", "repayed"]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 结果:如愿命中 {userId:1, status:1} 作为 winning plan。 step2:再建立个典型的索引 userId db.loans.createIndex({userId:1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 2, "numIndexesAfter" : 3, "ok" : 1 } db.loans.find({ "userId" : "59e022d33f239800129c61c7", "status" : "repayed", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "status" : { "$eq" : "repayed" } }, { "userId" : { "$eq" : "59e022d33f239800129c61c7" } } ] }, "queryHash" : "15D5A9A1", "planCacheKey" : "1B1A4861", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "[\"59e022d33f239800129c61c7\", \"59e022d33f239800129c61c7\"]" ], "status" : [ "[\"repayed\", \"repayed\"]" ] } } }, "rejectedPlans" : [ { "stage" : "FETCH", "filter" : { "status" : { "$eq" : "repayed" } }, "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1 }, "indexName" : "userId_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ] } } } ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 留意到 DB 检测到 {userId:1, status:1} 为更优执行的方案. db.loans.find({ "userId" : "59e022d33f239800129c61c7" }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "userId" : { "$eq" : "59e022d33f239800129c61c7" } }, "queryHash" : "B1777DBA", "planCacheKey" : "1F09D68E", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1 }, "indexName" : "userId_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ] } } }, "rejectedPlans" : [ { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ], "status" : [ "[MinKey, MaxKey]" ] } } } ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 留意到 DB 检测到 {userId:1} 为更优执行的方案,嗯~,如我们所料. db.loans.find({ "status" : "repayed" }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "status" : { "$eq" : "repayed" } }, "queryHash" : "E6304EB6", "planCacheKey" : "7A94191B", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "status" : { "$eq" : "repayed" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 有趣的部分:status不命中索引,全表扫描 接下来的步骤,加个sort : db.loans.find({ "userId" : "59e022d33f239800129c61c7" }).sort({status:1}).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "userId" : { "$eq" : "59e022d33f239800129c61c7" } }, "queryHash" : "F5ABB1AA", "planCacheKey" : "764CBAA8", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ], "status" : [ "[MinKey, MaxKey]" ] } } }, "rejectedPlans" : [ { "stage" : "SORT", "sortPattern" : { "status" : 1 }, "inputStage" : { "stage" : "SORT_KEY_GENERATOR", "inputStage" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1 }, "indexName" : "userId_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ] } } } } } ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } (二)其他尝试 有趣的部分:status 不命中索引 db.loans.find({ "status" : "repayed","userId" : "59e022d33f239800129c61c7", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "$and" : [ { "status" : { "$eq" : "repayed" } }, { "userId" : { "$eq" : "59e022d33f239800129c61c7" } } ] }, "queryHash" : "15D5A9A1", "planCacheKey" : "1B1A4861", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "[\"59e022d33f239800129c61c7\", \"59e022d33f239800129c61c7\"]" ], "status" : [ "[\"repayed\", \"repayed\"]" ] } } }, "rejectedPlans" : [ { "stage" : "FETCH", "filter" : { "status" : { "$eq" : "repayed" } }, "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1 }, "indexName" : "userId_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ] } } } ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 命中索引,跟query的各个字段顺序不相关,如我们猜测。 有趣部分再来, 我们删掉索引{userId:1} db.loans.dropIndex({"userId":1}) { "nIndexesWas" : 3, "ok" : 1 } db.loans.find({"userId" : "59e022d33f239800129c61c7", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "userId" : { "$eq" : "59e022d33f239800129c61c7" } }, "queryHash" : "B1777DBA", "planCacheKey" : "5776AB9C", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "["59e022d33f239800129c61c7", "59e022d33f239800129c61c7"]" ], "status" : [ "[MinKey, MaxKey]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } DB 执行分析器觉得索引{userId:1, status:1} 能更优,没有命中复合索引,这个是因为status不是leading field。 db.loans.find({ "status" : "repayed" }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "status" : { "$eq" : "repayed" } }, "queryHash" : "E6304EB6", "planCacheKey" : "7A94191B", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "status" : { "$eq" : "repayed" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 再换个角度sort 一遍, 与前面query & sort互换,之前是: db.loans.find({userId:1}).sort({ "status" : "repayed" }) 看看有啥不一样? db.loans.find({ "status" : "repayed" }).sort({userId:1}).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "status" : { "$eq" : "repayed" } }, "queryHash" : "56EA6313", "planCacheKey" : "2CFCDA7F", "winningPlan" : { "stage" : "FETCH", "filter" : { "status" : { "$eq" : "repayed" } }, "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "userId" : 1, "status" : 1 }, "indexName" : "userId_1_status_1", "isMultiKey" : false, "multiKeyPaths" : { "userId" : [ ], "status" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "userId" : [ "[MinKey, MaxKey]" ], "status" : [ "[MinKey, MaxKey]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 如猜测,命中索引。 再来玩玩,确认下leading filed试验: db.loans.dropIndex("userId_1_status_1") { "nIndexesWas" : 2, "ok" : 1 } db.loans.getIndexes() [ { "v" : 2, "key" : { "id" : 1 }, "name" : "id_", "ns" : "cashLoan.loans" } ] db.loans.createIndex({status:1, userId:1}) { "createdCollectionAutomatically" : false, "numIndexesBefore" : 1, "numIndexesAfter" : 2, "ok" : 1 } db.loans.getIndexes() [ { "v" : 2, "key" : { "id" : 1 }, "name" : "id_", "ns" : "cashLoan.loans" }, { "v" : 2, "key" : { "status" : 1, "userId" : 1 }, "name" : "status_1_userId_1", "ns" : "cashLoan.loans" } ] db.loans.find({ "status" : "repayed" }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "status" : { "$eq" : "repayed" } }, "queryHash" : "E6304EB6", "planCacheKey" : "7A94191B", "winningPlan" : { "stage" : "FETCH", "inputStage" : { "stage" : "IXSCAN", "keyPattern" : { "status" : 1, "userId" : 1 }, "indexName" : "status_1_userId_1", "isMultiKey" : false, "multiKeyPaths" : { "status" : [ ], "userId" : [ ] }, "isUnique" : false, "isSparse" : false, "isPartial" : false, "indexVersion" : 2, "direction" : "forward", "indexBounds" : { "status" : [ "["repayed", "repayed"]" ], "userId" : [ "[MinKey, MaxKey]" ] } } }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } db.loans.getIndexes() [ { "v" : 2, "key" : { "id" : 1 }, "name" : "id_", "ns" : "cashLoan.loans" }, { "v" : 2, "key" : { "status" : 1, "userId" : 1 }, "name" : "status_1_userId_1", "ns" : "cashLoan.loans" } ] db.loans.find({"userId" : "59e022d33f239800129c61c7", }).explain() { "queryPlanner" : { "plannerVersion" : 1, "namespace" : "cashLoan.loans", "indexFilterSet" : false, "parsedQuery" : { "userId" : { "$eq" : "59e022d33f239800129c61c7" } }, "queryHash" : "B1777DBA", "planCacheKey" : "5776AB9C", "winningPlan" : { "stage" : "COLLSCAN", "filter" : { "userId" : { "$eq" : "59e022d33f239800129c61c7" } }, "direction" : "forward" }, "rejectedPlans" : [ ] }, "serverInfo" : { "host" : "RMBAP", "port" : 27017, "version" : "4.1.11", "gitVersion" : "1b8a9f5dc5c3314042b55e7415a2a25045b32a94" }, "ok" : 1 } 看完这个试验,明白了 {userId:1, status:1} vs {status:1,userId:1} 的差别了吗? PS:这个case 里面其实status 区分度不高,这里只是作为实例展示。 三、总结: 注意使用上、使用频率上、区分高的/常用的在前面; 如果需要减少索引以节省memory/提高修改数据的性能的话,可以保留区分度高,常用的,去除区分度不高,不常用的索引。 学会用explain()验证分析性能: DB 一般都有执行器优化的分析,MySQL & MongoDB 都是 用explain 来做分析。 语法上MySQL : explain your_sql MongoDB: yoursql.explain() 总结典型: 理想的查询是结合explain 的指标,他们通常是多个的混合: IXSCAN : 索引命中; Limit : 带limit; Projection : 相当于非 select * ; Docs Size less is better ; Docs Examined less is better ; nReturned=totalDocsExamined=totalKeysExamined ; SORT in index :sort 也是命中索引,否则,需要拿到数据后,再执行一遍排序; Limit Array elements : 限定数组返回的条数,数组也不应该太多数据,否则schema 设计不合理。 彩蛋 文末,还有最开头1个问题没回答: 如果我的索引改加的都加了,还不够快,怎么办? 留个悬念,之后再写一篇。
「深度学习福利」大神带你进阶工程师,立即查看>>> 绑定参数里面,全部使用%s,不能用%d,如 cursor.execute( 'insert into phoneinfo(serial, time) values("%s", "%s")' , ([ '1' , 1 ])) time字段类型是int,后面传入的1也是int型,但是参数类型需要用%s
「深度学习福利」大神带你进阶工程师,立即查看>>> 今天重装了mysql,发现安装好了之后,root用户自带了一个随机密码,记录一下这个坑。 1、安装mysql,然后mysql目录加到系统环境变量 2、到mysql安装目录下,和bin同一个目录,创建目录data,用来放数据库文件 3、打开cmd,输入 mysqld --initialize 4、初始化完成后,启动mysql服务 net start mysql 5、启动完成后,然后进入mysql环境 mysql -u root mysql 然后发现,没有权限,要求输入密码!!! 据说是因为mysql启动的时候,不再分配无密码的数据库用户,所以自己生成了一个随机密码。 问题来了:这随机密码,他娘的在哪儿??? 6、进入刚刚最开始创建的data文件夹,跟bin同一个文件夹的那个,里面有一个.err的文件,用记事本打开。 搜索,temporary password,然后找到这一行 这里的root @localhost : 6kCXy%g;qLbu,root@localhost就是用户名,冒号后面的就是密码。 7、再次打开cmd,输入 mysql -uroot -p 把刚刚找到的那个密码复制进去,回车,成功进入mysql环境 8、修改密码 set password for root@localhost = password('123') 完事。
「深度学习福利」大神带你进阶工程师,立即查看>>> [root@rac2 ~]# oracleasm scandisks Reloading disk partitions: done Cleaning any stale ASM disks... Scanning system for ASM disks... [root@rac2 ~]# 解决方法: 是因为在rac1节点格式化磁盘没有使用partprobe命令,分区没有生效 在rac1执行: [root@rac1 ~]# partprobe 在rac2执行: [root@rac2 ~]# /usr/sbin/oracleasm scandisks Reloading disk partitions: done Cleaning any stale ASM disks... Scanning system for ASM disks... Instantiating disk "VOL04" Instantiating disk "VOL03" Instantiating disk "VOL01" Instantiating disk "VOL02" [root@rac2 ~]# /usr/sbin/oracleasm listdisks VOL01 VOL02 VOL03 VOL04 [root@rac2 ~]#
「深度学习福利」大神带你进阶工程师,立即查看>>> limit10000,20的意思扫描满足条件的10020行,扔掉前面的10000行,返回最后的20行,问题就在这里。 SELECT * FROM users WHERE uid >= (SELECT uid FROM users ORDER BY uid limit 895682, 1) limit 0, 10; 记录下最大id,然后扫描后面的