首页 文章 精选 留言 我的

精选列表

搜索[全场景互联],共10000篇文章
优秀的个人博客,低调大师

全栈角度看分页处理

作者:京东物流 杨攀 分页是 web application 开发最常见的功能。在使用不同的框架和工具过程中,发现初始行/页的定义不同,特意整理记录。从这个技术点去看不同层的实现。以及不同语言实现的对比。 文章会从正常的web 结构分层的角度去梳理不同层的处理。 分为数据库分页、服务端分页、前端分页 数据库分页 这里用mysql 举例整理。我们常用的数据库例如 Oracle/ SQL Server 等,对于分页语法的支持大同小异。不做具体一一举例。 先从数据库层梳理,也是从最根源去分析分页的最终目的,前端和后端的一起逻辑和适配,都是为了拼接合适的 SQL 语句。 ①MySQL LIMIT 语法:[LIMIT {[offset,]row_count}] LIMIT row_countis equivalent toLIMIT 0, row_count. The offset of the initial row is 0 (not 1) 参考:MySQL :: MySQL 5.7 Reference Manual :: 13.2.9 SELECT Statement 服务端/后端分页 后端分页,简单讲,就是数据库的分页。 对于mysql 来讲,就是上述 offset row_count 的计算过程。 这里选用了常用的框架组件来对比各自实现的细节。 pagehelper 是Java Orm 框架mybatis 常用的开源分页插件 spring-data-jdbc 是Java 框架常用的数据层组件 ①pagehelper /** * 计算起止行号 offset * @see com.github.pagehelper.Page#calculateStartAndEndRow */ private void calculateStartAndEndRow() { // pageNum 页码,从1开始。 pageNum < 1 , 忽略计算。 this.startRow = this.pageNum > 0 ? (this.pageNum - 1) * this.pageSize : 0; this.endRow = this.startRow + this.pageSize * (this.pageNum > 0 ? 1 : 0); } /** * 计算总页数 pages/ pageCount。 * 在赋值数据总条数的同时,也计算了总页数。 * 可以与 Math.ceil 实现对比看。 */ public void setTotal(long total) { if (pageSize > 0) { pages = (int) (total / pageSize + ((total % pageSize == 0) ? 0 : 1)); } else { pages = 0; } } SQL 拼接实现: com.github.pagehelper.dialect.helper.MySqlDialect ②spring-data-jdbc 关键类: org.springframework.data.domain.Pageable org.springframework.data.web.PageableDefault /** * offset 计算,不同于pagehelper, page 页码从0 开始。 default is 0 * @see org.springframework.data.domain.AbstractPageRequest#getOffset */ public long getOffset() { return (long)this.page * (long)this.size; } /* * 总页数的计算使用 Math.ceil 实现。 * @see org.springframework.data.domain.Page#getTotalPages() */ @Override public int getTotalPages() { return getSize() == 0 ? 1 : (int) Math.ceil((double) total / (double) getSize()); } /** * offset 计算,不同于pagehelper, page 页码从0 开始。 * @see org.springframework.data.jdbc.core.convert.SqlGenerator#applyPagination */ private SelectBuilder.SelectOrdered applyPagination(Pageable pageable, SelectBuilder.SelectOrdered select) { // 在spring-data-relation, Limit 抽象为 SelectLimitOffset SelectBuilder.SelectLimitOffset limitable = (SelectBuilder.SelectLimitOffset) select; // To read the first 20 rows from start use limitOffset(20, 0). to read the next 20 use limitOffset(20, 20). SelectBuilder.SelectLimitOffset limitResult = limitable.limitOffset(pageable.getPageSize(), pageable.getOffset()); return (SelectBuilder.SelectOrdered) limitResult; } spring-data-commons 提供 mvc 层的分页参数处理器 /** * Annotation to set defaults when injecting a {@link org.springframework.data.domain.Pageable} into a controller method. * * @see org.springframework.data.web.PageableDefault */ @Documented @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.PARAMETER) public @interface PageableDefault { /** * The default-size the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding * parameter defined in request (default is 10). */ int size() default 10; /** * The default-pagenumber the injected {@link org.springframework.data.domain.Pageable} should get if no corresponding * parameter defined in request (default is 0). */ int page() default 0; } MVC 参数处理器: org.springframework.data.web.PageableHandlerMethodArgumentResolver 前端分页 前端展示层,分别从服务端渲染方案以及纯前端脚本方案去看分页最终的页面呈现逻辑。 这里选取的分别是Java 常用的模板引擎 thymeleaf 以及热门的前端框架 element-ui。 从用法以及组件源码角度,去理清终端处理分页的常见方式。 ①thymeleaf - 模板引擎 Thymeleafis a modern server-side Java template engine for both web and standalone environments. <!-- spring-data-examples\web\example\src\main\resources\templates\users.html--> <nav> <!-- class样式 bootstrap 默认的分页用法--> <ul class="pagination" th:with="total = ${users.totalPages}"> <li th:if="${users.hasPrevious()}"> <a th:href="@{/users(page=${users.previousPageable().pageNumber},size=${users.size})}" aria-label="Previous"> <span aria-hidden="true">«</span> </a> </li> <!-- spring-data-examples 分页计算从0 开始, /users?page=0&size=10 --> <!-- 生成页码列表,呈现形式即我们常见的 ①②③④ --> <li th:each="page : ${#numbers.sequence(0, total - 1)}"><a th:href="@{/users(page=${page},size=${users.size})}" th:text="${page + 1}">1</a></li> <li th:if="${users.hasNext()}"> <!-- 下一页实现,因为是服务器端渲染生成。在最终生成的html 中,这里的href 是固定的。实现思路和纯前端技术,使用javascript 脚本对比看 --> <a th:href="@{/users(page=${users.nextPageable().pageNumber},size=${users.size})}" aria-label="Next"> <span aria-hidden="true">»</span> </a> </li> </ul> </nav> ②element-ui 前端框架 // from node_modules\element-ui\packages\pagination\src\pagination.js // page-count 总页数,total 和 page-count 设置任意一个就可以达到显示页码的功能; computed: { internalPageCount() { if (typeof this.total === 'number') { // 页数计算使用 Math.ceil return Math.max(1, Math.ceil(this.total / this.internalPageSize)); } else if (typeof this.pageCount === 'number') { return Math.max(1, this.pageCount); } return null; } }, /** * 起始页计算。 page 页码从1 开始。 */ getValidCurrentPage(value) { value = parseInt(value, 10); // 从源码的实现可以看到,一个稳定强大的开源框架,在容错、边界处理的严谨和思考。 const havePageCount = typeof this.internalPageCount === 'number'; let resetValue; if (!havePageCount) { if (isNaN(value) || value < 1) resetValue = 1; } else { // 强制赋值起始值 1 if (value < 1) { resetValue = 1; } else if (value > this.internalPageCount) { // 数据越界,强制拉回到PageCount resetValue = this.internalPageCount; } } if (resetValue === undefined && isNaN(value)) { resetValue = 1; } else if (resetValue === 0) { resetValue = 1; } return resetValue === undefined ? value : resetValue; }, 总结 技术永远是关联的,思路永远是相似的,方案永远是相通的。单独的去分析某个技术或者原理,总是有边界和困惑存在。纵向拉伸,横向对比才能对技术方案有深刻的理解。在实战应用中,能灵活自如。 分页实现的方案最终是由数据库决定的,对于众多的数据库,通过SQL 语法的规范去框定,以及我们常用的各种组件或者插件去适配。 纵向对比,我们可以看到不同技术层的职责和通用适配的实现过程,对于我们日常的业务通用开发以及不同业务的兼容有很大的借鉴意义。 横向对比,例如前端展示层的实现思路,其实差别非常大。如果使用 thymeleaf,结构简单清晰,但交互响应上都会通过服务器。如果选用element-ui,分页只依赖展示层,和服务端彻底解构。在技术选型中可以根据各自的优缺点进行适度的抉择。

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

Elasticsearch API 使用指南(全)

1. 集群支持的选项 curl -XGET "http://172.0.0.52:9200/_cat" 2. 查看节点信息 curl -XGET "http://172.0.0.52:9200/_cat/nodes?v" 3. 查看master节点信息 curl -XGET "http://172.0.0.52:9200/_cat/master?v" 4.查看所有节点上的热点线程 curl -XGET "http://172.0.0.52:9200/_nodes/hot_threads" 5.查看有问题的分片或索引 curl -XGET "http://172.0.0.52:9200/_cluster/allocation/explain?pretty" 6.查看线程池设置 curl -XGET "http://172.0.0.52:9200/_nodes/thread_pool/" 7.统计全部信息 curl -XGET "http://172.0.0.52:9200/_cluster/stats?human&pretty" 8.查看集群状态 curl -XGET "http://172.0.0.52:9200/_cluster/health?pretty" 9.查看ES信息 curl -XGET "http://172.0.0.52:9200/" 10.获取所有索引的信息 curl -XGET "http://172.0.0.52:9200/_cat/indices?v&pretty" curl -XGET "http://172.0.0.52:9200/_cat/nodes?v" curl -XGET "http://172.0.0.52:9200/_cat/segments?v&h=shard,segment,size,size.memory" 11.获取所有文档数量 curl -XGET "http://172.0.0.52:9200/_count?pretty" -H 'Content-Type: application/json' -d' { "query": { "match_all": {} } }' 12. 查看集群的健康状态 green:所有功能都是完好的; yellow:所有数据是可用的,但是一些副本还没有被分配; red代表一些数据由于某些原因已经不可用。 注意,尽管一个集群是red状态,它仍然可以提供部分服务(比如,它会继续从可用的切片数据里搜索),但是在失去部分数据后,需要尽快去修复。 curl -XGET "http://172.0.0.52:9200/_cat/health?v" 13. 创建索引 test_one 索引名 pretty 参数表示输出格式良好的JSON响应(如果存在) curl -XPUT "http://172.0.0.52:9200/test_one?pretty" 14. 查看索引列表 curl -XGET "http://172.0.0.52:9200/_cat/indices?v" curl -XGET "http://172.0.0.52:9200/test_one" 15. 删除索引 根据索引名称删除 curl -XDELETE "http://172.0.0.52:9200/test_one?pretty" 可以一次删除多个索引(以逗号间隔)删除所有索引_all或通配符 * 16. 创建文档 向es中插入文档的时候,必须要指定一个类型(type) 16.1.使用PUT来创建文档,需要指定id 索引 index:test_one 类型 type:test_type _id:1 curl -XPUT "http://172.0.0.52:9200/test_one/test_type/1?pretty" -H 'Content-Type: application/json' -d' {"name": "ghl", "age": 24, "sex": "male"}' 16.2. 使用POST来创建文档,可以不指定id(不指定时随机生成id) curl -XPOST "http://172.0.0.52:9200/test_one/test_type?pretty" -H 'Content-Type: application/json' -d' {"name": "Jack"}' 17. 查看文档 curl -XGET "http://172.0.0.52:9200/test_one/test_type/1?pretty" 18. 替换文档 使用PUT并指定id时,es会使用新的文档替换原文档 curl -XPUT "http://172.0.0.52:9200/test_one/test_type/1?pretty" -H 'Content-Type: application/json' -d' {"name": "Jack"}' 19. 更新文档 curl -XPOST "http://172.0.0.52:9200/test_one/test_type/1/_update?pretty" -H 'Content-Type: application/json' -d' {"doc":{"name": "Lucy"}}' 20. 删除文档 curl -XDELETE "http://172.0.0.52:9200/test_one/test_type/1?pretty" 21. 索引的增删改查有一个类似的格式下: curl -XGET "http://172.0.0.52:9200/<Index>/<Type>/<ID>" REST Verb:REST风格的语法谓词; Node:节点ip; port:节点端口号,默认9200; Index:索引名; Type:索引类型; ID:操作对象的ID号; 22. 判断索引是否存在 curl -XHEAD "http://172.0.0.52:9200/test_one" 23. 查看索引模板 curl -XGET "http://172.0.0.52:9200/_template/template_1" curl -XGET "http://172.0.0.52:9200/_template/temp*" curl -XGET "http://172.0.0.52:9200/_template/template_1,template_2" curl -XGET "http://172.0.0.52:9200/_template" 24. 删除模板 curl -XDELETE "http://172.0.0.52:9200/_template/template_1" 25. 打开/关闭索引 curl -XPOST "http://172.0.0.52:9200/test_one/_close" curl -XPOST "http://172.0.0.52:9200/test_one/_open" curl -XGET "http://172.0.0.52:9200/_cat/indices?v" 26.查看索引状态信息 curl -XGET "http://172.0.0.52:9200/_stats" curl -XGET "http://172.0.0.52:9200/logstash-nginx-access-2019.08.07,test_one/_stats" 27.查看索引段信息 curl -XGET "http://172.0.0.52:9200/test_one/_segments" curl -XGET "http://172.0.0.52:9200/logstash-nginx-access-2019.08.07,test_one/_segments" curl -XGET "http://172.0.0.52:9200/_segments"

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

全排列

说起回溯算法,渊源颇深,我上次做过一道,本应该这次能拿下但是,又没拿下,不过这次有点理解它的思想了, 每次的回溯就是一个二叉树,简单的说,就是你执行的操作需要回溯时它会返回调用它的方法哪里的状态,进行下一步的执行。 public List<List<Integer>> permute(int[] nums) { List<List<Integer>> temp = new ArrayList<>(20); zuhe(temp,0,nums); return temp; } private void zuhe(List<List<Integer>> temp, int n, int[] nums) { /* * 每次让其加一个就行了,就空了, * */ if (nums.length == n) { List<Integer> integers =new ArrayList<>(20); for (int r:nums){ integers.add(r); } temp.add(integers); } for (int i = n, length = nums.length; i <length ; i++) { change(nums,i,n); zuhe(temp,n+1,nums); change(nums,i,n); } } public void change(int[] nums, int i, int j) { int temp = nums[i]; nums[i] = nums[j]; nums[j] = temp; } 这个需要多练,具体自己体会

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

Python全栈工程师

ParisGabriel Python 入门基础 print(“hello world”)变量 : 存储信息的,日后被调用、修改操作常量: 固定不变的量,字母大写命名规则:1. 字母数字下划线组成2. 不能以数字开头,不能含有特殊字符和空格3. 不能以保留字命名4. 不能以中文命名5. 定义的变量名应该有意义6. 驼峰式命、 下划线分割单词7. 变量名区分大小写a=1b=2if a<b: print("Yes") print("Yes") print("Yes") print("Yes")else: print("No")a=1b=2 if a>b: print("Yes") elif a==b: print("第三") else: print("any") if 条件1: 自拍elif 条件2: 蹦 else: 跳舞 # 单行注释'''多行注释'''""" 多行注释 """ input() "abc" + "qwe" file.py 文件的扩展名:.py : python的程序文件.txt : 文本文件pdf chm html doc xml xls ppt jpg png gif jpeg bmpavi rmvb MP4 mkv 3gp WMV MP3 flue mid True 真 正确的False 假 错误的 a b = 100c = 1000 if b <= a <= c: print("True") num number num1 = intpu("Num1:")num2 = intpu("Num2:")num3 = intpu("Num3:") 输出三个数字中的最大值/最小值 if num1>num2>num3: #num1最大else: #num1<num2 if num1>num2>num3 #num1最大elif num1>num3>num2 #num1最大elif num2>num1>num3 #num2最大elif num2>num3>num1 #num2最大elif num3>num2>num1 #num3最大else num3>num1>num2 #num3最大 num1 num2 num3 max_num =0if num1>num2: max_num= num1 if max_num > num3: print("Max NUM is",max_num) else: print("Max NUM is",num3)else: max_num = num2 if max_num > num3: print("Max NUM is",max_num) else: print("Max NUM is",num3) num += 1 等价于 num = num + 1num -= 1 等价于 num = num - 1num *= 2 等价于 num = num * 2num /= 2 等价于 num = num / 2num //= 2 等价于 num = num // 2num %= 2 等价于 num = num % 2num **= 2 等价于 num = num ** 2 and 且,并且只有两个条件全部为True(正确)的时候, 结果才会为True(正确) 条件1 and 条件2 5>3 and 6<2 True or 或,或者只要有一个条件为True,则结果为Ture,5>3 or 6<2 真 或 假 not 不,雅蠛蝶 not 5>3 == Falsenot 5<3 == True a>b and (c>d or (not f)) (not (not True)) or (False and (not True)) 条件1 and 条件2条件1 or 条件2 短路原则对于and 如果前面的第一个条件为假,那么这个and前后两个条件组成的表达式 的计算结果就一定为假,第二个条件就不会被计算 对于or 如果前面的第一个条件为真,那么这个or前后两个条件组成的表达式 的计算结果就一定为真,第二个条件就不会被计算 True or True and False 猜年龄 age = 50 user_input_age = int(input("Age is :"))if ....while 循环 while 条件: print("any") print("any") num = 1 while num<10: # 2 print(num) # 2 num+=1 # 3 if num == 9: # 3 breaknum = 1 while num<=100: # num<=100 等价于 True # while num<=100: 等价于 while True: if num%2 == 0: print(num) num += 1num = 1 while num<=100: if num%2 == 1: print(num) num += 1 age = 50 #user_input_age = int(input("Age is :")) flag = True while flag: user_input_age = int(input("Age is :")) if user_input_age == age: print("Yes") flag =False elif user_input_age > age: print("Is bigger") else: print("Is smaller") print("End") break # 终止age = 50 #user_input_age = int(input("Age is :")) #flag = True# break while True: user_input_age = int(input("Age is :")) if user_input_age == age: print("Yes") break elif user_input_age > age: print("Is bigger") else: print("Is smaller") print("End") continue 继续 if a>b and d<f or 5>3 and d == e: ...... while 条件: ....else: .... statement 语句 num = 1while num <= 10: num += 1 if num == 5: break print(num)else: print("This is else statement") while 条件1: ..... while 条件2: .... 1*1=11*2=2 2*2=41*3=3 2*3=6 3*3=9.... #print("hello world.",end="__") # \n \r\n \r#print("hello world.",end="__")#print("hello world.",end="__") num1 = 0 while num1<=5: print(num1,end="_") num2 = 0 while num2<=7: print(num2,end="-") num2+=1 num1+=1 print() # print(end="\n") #0_0-1-2-3-4-5-6-7-#1_0-1-2-3-4-5-6-7- Height 高度width 宽度 height = int(input("Height:")) # 用户输入一个高度width = int(input("width:")) # 用户输入一个宽度 num_height = 1while num_height <=height: num_width = 1 while num_width <= width: print("#", end="") num_width += 1 print() num_height += 1 12345678223456783234567842345678 第一行的时候 8字符 8次循环第二行的时候 8字符 8次循环第三行的时候 8字符 8次循环第四行的时候 8字符 8次循环 width = int(input("width:")) num_width = 1 while num_width<=width: print("#", end="") num_width +=1print() num_width = 1while num_width<=width: print("#", end="") num_width +=1print() num_width = 1while num_width<=width: print("#", end="") num_width +=1print() num_width = 1while num_width<=width: print("#", end="") num_width +=1 #print("#",end="")#print("#",end="")#print("#",end="")#print("#",end="") print() num = 4 while num>0: print("#", end="") num -= 1 print() # @ == #### height = int(input("Height:")) # 用户输入一个高度width = int(input("width:")) # 用户输入一个宽度 #num2 = height num2 = height # 第一步: 赋值while num2 > 0: # 第二步 :num2 == 2 num1 = width # 第三步: 赋值 while num1>0: # 第四部:num1==2 # 第七步:num1 = 1 print("#", end="") # 第五步: 不换行 打印一个# 第八步: 不换行 打印一个# num1 -= 1 #第六步: num1 = 1 第九步: num1 = 0 print() # 第十步 : 只是换行 num2 -= 1 # 第十一步 : num2=1 #### print("1*1=",1) # "1*1=",1 == str(m)+"*"+str(n)+"=",1 m = 2n = 2 print( str(m)+ "*" + str(n) + "=" , m*n )line = 5 # 第一步 : 赋值while line>0: # 第二部 line=5 tmp = line # 第三部 : tmp =5 tmp=4 while tmp>0: # 第四部 : tmp =5 #第七步 tmp=4 #第十部: tmp=3 第十三步 tmp=2 print("*",end="") #第五步 #第八部 #第十一部 #第十四步 tmp = tmp-1 # 第六步 tmp = 4 # 第九步 tmp=3 # 第十二步 tmp=2 第十五步 tmp= 1 print() #print(line) line -= 1first = 1while first<=9: sec = 1 while sec <= first: print( str(sec)+"*"+ str(first) +"="+str(sec * first), end="\t") sec += 1 print() first += 1 今天太晚了明天要上课 明天晚上补图片 代码

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

mysql全表抽取到MPP

代码实例 package com.epoint.com.mysql_mpp_full; import java.io.FileNotFoundException; import java.sql.Connection; import java.sql.DriverManager; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.sql.Statement; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.TimeZone; public class AutoMysqltoMPP { private static String MYSQLUSERNAME = "root"; private static String MYSQLPASSWORD = "Gepoint"; private static String MYSQLDRIVER = "com.mysql.jdbc.Driver"; private static String MYSQLURL = "jdbc:mysql://100.2.5.221:3307/dep_fr_db"; private static String MYSQLDATABASE = "dep_fr_db"; private static String MPPDRIVER = "com.MPP.jdbc.Driver"; private static String MPPURL = "jdbc:MPP://100.2.5.1:5258/dep_fr_db"; private static String MPPUSERNAME = "mpp"; private static String MPPPASSWORD = "h3c"; private static Connection mysqlconn = null; private static Statement mysqlpstm = null; private static ResultSet mysqlrs = null; private static Connection mppconn = null; private static Statement mppstm = null; private static ResultSet mpprs = null; String sql1 = " "; String sql2 = " "; String sql3 = " "; String sql4 = " "; String sql5 = " "; String sql6 = " "; public static void main(String[] args) throws Exception { AutoMysqltoMPP aidth = new AutoMysqltoMPP(); aidth.getMYSQLConnection(); aidth.MYSQLReleaseResource(); aidth.getMPPConnection(); aidth.MPPReleaseResource(); aidth.CreateMPPTable(); // aidth.ImportDataToMPP(); System.out.println("表已创建完毕,赶紧去查看表吧!!"); SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss:SS"); TimeZone t = sdf.getTimeZone(); t.setRawOffset(0); sdf.setTimeZone(t); Long startTime = System.currentTimeMillis(); // 此段为要放置测取时间的函数 mysqlconn = getMYSQLConnection(); mppconn = getMPPConnection(); mppstm = mppconn.createStatement(); mysqlpstm = mysqlconn.createStatement(); mppstm.execute("TRUNCATE table code_main"); aidth.tableInput(); Long endTime = System.currentTimeMillis(); System.out.println("用时:" + sdf.format(new Date(endTime - startTime))); } public void CreateMPPTable() { mysqlconn = getMYSQLConnection(); mppconn = getMPPConnection(); try { mppstm = mppconn.createStatement(); mysqlpstm = mysqlconn.createStatement(); int i = 0; String sql = "SELECT table_schema\r\n" + " ,table_name\r\n" + " ,(\r\n" + " CASE \r\n" + " WHEN ORDINAL_POSITION = mincol\r\n" + " AND ORDINAL_POSITION < maxcol\r\n" + " THEN CONCAT (\"create table if not exists \"\r\n" + " ,table_schema\r\n" + " ,\".\"\r\n" + " ,table_name\r\n" + " ,\"(`\"\r\n" + " ,column_name\r\n" + " ,\"` \"\r\n" + " ,COLUMN_TYPE\r\n" + " ,\",\"\r\n" + " )\r\n" + " WHEN ORDINAL_POSITION = mincol\r\n" + " AND ORDINAL_POSITION = maxcol\r\n" + " THEN CONCAT (\"create table if not exists \"\r\n" + " ,table_schema\r\n" + " ,\".\"\r\n" + " ,table_name\r\n" + " ,\"(`\"\r\n" + " ,column_name\r\n" + " ,\"` \"\r\n" + " ,COLUMN_TYPE\r\n" + " ,\");\"\r\n" + " )\r\n" + " WHEN ORDINAL_POSITION > mincol\r\n" + " AND ORDINAL_POSITION < maxcol\r\n" + " THEN CONCAT (\r\n" + " \"`\"\r\n" + " ,column_name\r\n" + " ,\"` \"\r\n" + " ,COLUMN_TYPE\r\n" + " ,\",\"\r\n" + " )\r\n" + " WHEN ORDINAL_POSITION = maxcol\r\n" + " THEN CONCAT (\r\n" + " \"`\"\r\n" + " ,column_name\r\n" + " ,\"` \"\r\n" + " ,COLUMN_TYPE\r\n" + " ,\");\"\r\n" + " )\r\n" + " END\r\n" + " ) AS statement\r\n" + " ,ORDINAL_POSITION\r\n" + " ,maxcol\r\n" + " ,mincol\r\n" + "FROM (\r\n" + " SELECT b.table_schema,b.table_name,b.ORDINAL_POSITION,b.column_name,\r\n" + " (case\r\n" + " when column_type = 'timestamp' then 'datetime'\r\n" + " when column_type = 'bit(1)' then 'int(1)'\r\n" + " else\r\n" + " column_type\r\n" + " end ) AS column_type\r\n" + " ,a.maxcol\r\n" + " ,a.mincol\r\n" + " FROM (\r\n" + " SELECT table_schema\r\n" + " ,table_name\r\n" + " ,max(ORDINAL_POSITION) maxcol\r\n" + " ,min(ORDINAL_POSITION) mincol\r\n" + " FROM information_schema.COLUMNS\r\n" + " GROUP BY table_schema\r\n" + " ,table_name\r\n" + " ) a\r\n" + " JOIN (\r\n" + " SELECT table_schema\r\n" + " ,table_name\r\n" + " ,ORDINAL_POSITION\r\n" + " ,column_name\r\n" + " ,COLUMN_TYPE\r\n" + " FROM information_schema.COLUMNS\r\n" + " ORDER BY table_schema\r\n" + " ,table_name\r\n" + " ,ORDINAL_POSITION ASC\r\n" + " ) b ON a.table_schema = b.table_schema\r\n" + " AND a.table_name = b.table_name\r\n" + " ) c\r\n" + "WHERE table_schema = '" + MYSQLDATABASE + "'"; mysqlrs = mysqlpstm.executeQuery(sql); while (mysqlrs.next()) { sql1 = mysqlrs.getString(3); sql2 = sql2 + sql1; } sql3 = "create database IF NOT EXISTS " + MYSQLDATABASE; mppstm.execute(sql3); System.out.println("-------------------建mpp表,表结构的语句为:" + sql2); String[] sqls = sql2.split(";"); for (String m : sqls) { mppstm.execute(m); } System.out.println("----------------------------------------建mpp表已结束!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); mppstm.close(); mysqlpstm.close(); } catch (SQLException e) { e.printStackTrace(); } finally { MYSQLReleaseResource(); MPPReleaseResource(); } } public void ImportDataToMPP() { mysqlconn = getMYSQLConnection(); mppconn = getMPPConnection(); String sql = "select table_name from user_tables where num_rows > 0 order by table_name asc"; int i = 0; try { mysqlpstm = mysqlconn.createStatement(); mysqlrs = mysqlpstm.executeQuery(sql); mppstm = mppconn.createStatement(); while (mysqlrs.next()) { i = i + 1; String table_name = mysqlrs.getString("table_name").replaceAll("\\$", ""); String sql7 = "insert into " + MYSQLDATABASE + "." + table_name + " select * from " + MYSQLDATABASE + "_ex." + table_name; System.out.println("现在插入第" + i + "个表:" + sql7); mppstm.execute(sql7); } } catch (SQLException e) { e.printStackTrace(); } finally { MYSQLReleaseResource(); MPPReleaseResource(); } } public static List<List<String>> tableInput() throws FileNotFoundException, SQLException { List<List<String>> FindList = new ArrayList<List<String>>(); mysqlconn = getMYSQLConnection(); mppconn = getMPPConnection(); mppstm = mppconn.createStatement(); mysqlpstm = mysqlconn.createStatement(); PreparedStatement pre = null; ResultSet resultSet = null; String sql = "SELECT CODEID,CODENAME,LEVNUM,CATEGORYNUM,description,isfromsoa FROM code_main"; try { pre = mysqlconn.prepareStatement(sql); resultSet = pre.executeQuery(); String[] columu = { "CODEID","CODENAME","LEVNUM","CATEGORYNUM","description","isfromsoa"}; int i = 0; while (resultSet.next()) { List<String> minList = new ArrayList<String>(); for (String each : columu) { minList.add(resultSet.getString(each)); } FindList.add(minList); i++; if (i % 10000 == 0) { // 设置的每次提交大小为10000 executeManySql(FindList); FindList.removeAll(FindList); System.out.println(i); } } executeManySql(FindList);// 最后别忘了提交剩余的 return FindList; } catch (SQLException e) { // TODO Auto-generated catch block e.printStackTrace(); } finally { try { pre.close();// 关闭Statement } catch (SQLException e) { e.printStackTrace(); } try { mppstm.close(); mysqlpstm.close(); } catch (SQLException e) { e.printStackTrace(); } } return null; } //executeManySql(FindList)函数如下,因为数据量比较大,所以我设置的每次提交大小为10000,这样就不会内存溢出了。 public static void executeManySql(List<List<String>> FindList) throws SQLException { mysqlconn = getMYSQLConnection(); mppconn = getMPPConnection(); mppstm = mppconn.createStatement(); mysqlpstm = mysqlconn.createStatement(); mppconn.setAutoCommit(false); Statement stat = null; PreparedStatement pst = (PreparedStatement) mppconn .prepareStatement("insert into code_main values (?,?,?,?,?,?)"); for (List<String> minList : FindList) { for (int i = 0; i < minList.size(); i++) { pst.setString(i + 1, minList.get(i)); } // 把一个SQL命令加入命令列表 pst.addBatch(); } // 执行批量更新 pst.executeBatch(); // 语句执行完毕,提交本事务 mppconn.commit(); pst.close(); mppstm.close(); mysqlpstm.close();// 一定要记住关闭连接,不然mysql回应为too many connection自我保护而断开。 }//同时我还设置了计时的函数,可以看到这个从数据抽取到完成数据迁移的时间。 public static Connection getMYSQLConnection() { try { Class.forName(MYSQLDRIVER); mysqlconn = DriverManager.getConnection(MYSQLURL, MYSQLUSERNAME, MYSQLPASSWORD); } catch (ClassNotFoundException e) { throw new RuntimeException("class not find !", e); } catch (SQLException e) { throw new RuntimeException("get connection error!", e); } return mysqlconn; } public void MYSQLReleaseResource() { if (mysqlrs != null) { try { mysqlrs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (mysqlpstm != null) { try { mysqlpstm.close(); } catch (SQLException e) { e.printStackTrace(); } } if (mysqlconn != null) { try { mysqlconn.close(); } catch (SQLException e) { e.printStackTrace(); } } } public static Connection getMPPConnection() { try { Class.forName(MPPDRIVER); mppconn = DriverManager.getConnection(MPPURL, MPPUSERNAME, MPPPASSWORD); } catch (ClassNotFoundException e) { throw new RuntimeException("class not find !", e); } catch (SQLException e) { throw new RuntimeException("get connection error!", e); } return mppconn; } public void MPPReleaseResource() { if (mpprs != null) { try { mpprs.close(); } catch (SQLException e) { e.printStackTrace(); } } if (mppstm != null) { try { mppstm.close(); } catch (SQLException e) { e.printStackTrace(); } } if (mppconn != null) { try { mppconn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

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

2017阿里技术年度精选(全)

image.png 2017年,在技术发展的历史上,一定是个特别的一年:柯洁与AlphaGo的惊世大战,无人咖啡店开放体验,AI设计师“鲁班”横空出世、三年投入千亿的达摩院正式成立…… 这次,在阿里全年发布的近300篇文章中,阿里选出65篇,集结成这套 《2017 阿里技术年度精选》,分为上、下两册,总计600余页。 这套精选集覆盖多个热门技术领域:算法、机器学习、大数据、数据库、中间件、运维、安全、移动开发等,文章内容涉及技术架构、核心算法、解决方案等干货。无论你是计算机相关专业的在校学生、科研机构的 研究人员,还是步入社会的IT从业人员,相信都能从中受益。 这套书同时收录了十多位阿里技术人的访谈:从工程师到合伙人的多隆,6年时间影响数亿用户的靖世,入选MIT2017年度TR35的王刚&吴翰清,免试晋升为研究员的钱磊等,将为你展现不一样的技术人生。 它不是一本系统讲述某个领域的书,更像是一本技术杂文选集,内容五花八门。翻开书来,一眼望去,皆是散落在各个技术领域的结晶。你可以 把它当作床头书,或是在旅行的路上随手翻翻,充充电。希望这本书,能为 你打开一扇窗,去看更大的世界;成为一个小支点,帮你撬动更大的进步。 这套书的目录如下: 存储 / 数据库 运维 中间件 大牛观点 阿里开源 技术人生 这是总体目录,具体细节请查看这套全书。 如何获取此套书? 关注"阿里技术"微信公众号,回复“精选”获取,或者直接在" Java技术栈 "QQ群中下载,作者已经下载并将上、下两册打包在群文件中。 image.png 推荐阅读 个人珍藏最全Spring Boot全套视频教程 分享一套高级视频教程:Dubbo+Zookeeper+ActiveMQ+Redis系列 分享一套分布式架构设计高级视频教程 分享一套Hadoop全套视频教程系列 去BAT面试完的Mysql面试题总结(55道,带完整答案) 阿里高级Java面试题(首发,70道,带详细答案) 2017派卧底去阿里、京东、美团、滴滴带回来的面试题及答案 Spring面试题(70道,史上最全) 通往大神之路,百度Java面试题前200页。 分享Java干货,高并发编程,热门技术教程,微服务及分布式技术,架构设计,区块链技术,人工智能,大数据,Java面试题,以及前沿热门资讯等。

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

Docker-compose 使用全解

一、Compose安装 在安装compose之前,要确保已经安装了docker1.3或以上版本 在Linux64位系统上安装compose: curl-Lhttps://github.com/docker/compose/releases/download/1.1.0/docker-compose-`uname-s`-`uname-m`>/usr/local/bin/docker-compose chmod+x/usr/local/bin/docker-compose1212 注:当然可以选择安装command completion(见二) uname -s和uname -m中的两个引号是键盘上ESC下面的那个按键 此时,compose已经安装成功,使用命令docker-compose --version可以查看 如果是在OS X系统上,则需要执行如下步骤(未亲测): 二、Compose命令补全 安装命令补全 确保bash completion已经安装,如果当前使用非最小安装的Linux,bash completion已经OK了,如果是在MAC上,可以使用brew install bash-completion来安装 将completion脚本放在/etc/bash_completion.d/(在MAC上是/usr/local/etc/bash_completion.d/) curl-Lhttps://raw.githubusercontent.com/docker/compose/1.1.0/contrib/completion/bash/docker-compose>/etc/bash_completion.d/docker-compose11 在下次登录时,Completion功能已经可以使用 可用的补全取决于在命令行的输入,会补全: * 可用的Docker-compose命令 * 对于某一特别命令可用的选项 * 在一个给定的上下文条件(比如:具有运行或停止状态的实例的服务或者基于镜像的服务 VS 基于Dockerfile的服务)下,给出有可行的服务名称,对于docker-compose scale,补全服务名称时会自动有”=”附加上去 * 对于可选项的参数,比如:docker-compose kill -s会完成一些信号,比如SIGUP和SIGUSR1 拥有了这项功能以后,Compose更快更少输入了呢!Happy working! 三、Compose使用实例 在本例中将会实现启动nginx服务及一个数据卷容器,并将该数据卷容器作为nginx的静态文件 1.创建compose文件夹sudo mkdir composetestcd composetest 2.创建docker-compose.yml文件touch docker-compose.ymlvim docker-compose.yml 在docker-compose.yml中输入以下内容: dvc: image:debian:wheezy volumes: -/www:/usr/share/nginx/html:ronginx: image:nginx:latest volumes_from: -dvcports: -"8081:80"12345678910111234567891011 3.启动docker-compose up -d 注:使用命令docker-compose ps查看运行状况 四、CLI 说明(docker-compose 命令) 大多数Compose命令都是运行于一个或多个服务的,如果服务没有指定,该命令将会应用到所有服务,如果要获得所有可用信息,使用命令:docker-compose [COMMAND] --help,下面是命令(COMMAND)的说明: build 创建或者再建服务 服务被创建后会标记为project_service(比如composetest_db),如果改变了一个服务的Dockerfile或者构建目录的内容,可以使用docker-compose build来重建它 help 显示命令的帮助和使用信息 kill 通过发送SIGKILL的信号强制停止运行的容器,这个信号可以选择性的通过,比如:docker-compose kill -s SIGKINT logs 显示服务的日志输出 port 为端口绑定输出公共信息 ps 显示容器 pull 拉取服务镜像 rm 删除停止的容器 run 在服务上运行一个一次性命令,比如:docker-compose run webPythonmanage.py shell scale 设置为一个服务启动的容器数量,数量是以这样的参数形式指定的:service=num,比如:docker-compose scale web=2 worker=3 start 启动已经存在的容器作为一个服务 stop 停止运行的容器而不删除它们,它们可以使用命令docker-compose start重新启动起来 up 为一个服务构建、创建、启动、附加到容器 连接的服务会被启动,除非它们已经在运行了 默认情况下,docker-compose up会集中每个容器的输出,当存在时,所有的容器会停止,运行docker-compose up -d会在后台启动容器并使它们运行 默认情况下,如果服务存在容器的话,docker-compose up会停止并再创建它们(使用了volumes-from会保留已挂载的卷),如果不想使容器停止并再创建的话,使用docker-compose up --no-recreate,如果有需要的话,这会启动任何停止的容器 选项 –verbose 显示更多输出 –version 显示版本号并退出 -f,–file FILE 指定一个可选的Compose yaml文件(默认:docker-compose.yml) -p,–project-name NAME 指定可选的项目名称(默认:当前目录名称) 五、docker-compose.yml命令说明 每一个定义在docker-compose.yml中的服务必须明确指定一个image或者build选项,这与docker run命令行中输入的是对应相同的,对于docker run,在Dockerfile文件中指定的选项(比如CMD、EXPOSE、VOLUME、ENV)是默认的,因此不必在docker-compose.yml中再指定一次 image 标明image的ID,这个image ID可以是本地也可以是远程的,如果本地不存在,Compose会尝试去pull下来 image:ubuntuimage:orchardup/postgresqlimage:a4bc65fd123123 build 该参数指定Dockerfile文件的路径,该目录也是发送到守护进程的构建环境(这句有点),Compose将会以一个已存在的名称进行构建并标记,并随后使用这个image build:/path/to/build/dir11 command 重写默认的命令 command:bundleexecthin-p300011 links 连接到其他服务中的容器,可以指定服务名称和这个链接的别名,或者只指定服务名称 links: -db -db:database -redis12341234 此时,在容器内部,会在/etc/hosts文件中用别名创建一个条目,就像这样: 172.17.2.186db 172.17.2.186database 172.17.2.186redis123123 环境变量也会被创建,关于环境变量的参数,会在后面讲到 external_links 连接到在这个docker-compose.yml文件或者Compose外部启动的容器,特别是对于提供共享和公共服务的容器。在指定容器名称和别名时,external_links遵循着和links相同的语义用法 external_links: -redis_1 -project_db_1:mysql -project_db_1:postgresql12341234 ports 暴露端口,指定两者的端口(主机:容器),或者只是容器的端口(主机会被随机分配一个端口) 注:当以 主机:容器 的形式来映射端口时,如果使容器的端口小于60,那可能会出现错误,因为YAML会将 xx:yy这样格式的数据解析为六十进制的数据,基于这个原因,时刻记得要将端口映射明确指定为字符串 ports: -"3000"-"8000:8000"-"49100:22"-"127.0.0.1:8001:8001"1234512345 expose 暴露端口而不必向主机发布它们,而只是会向链接的服务(linked service)提供,只有内部端口可以被指定 expose: -"3000"-"8000"123123 volumes 挂载路径最为卷,可以选择性的指定一个主机上的路径(主机:容器),或是一种可使用的模式(主机:容器:ro) volumes_from: -service_name -container_name123123 environment 加入环境变量,可以使用数组或者字典,只有一个key的环境变量可以在运行Compose的机器上找到对应的值,这有助于加密的或者特殊主机的值 environment: RACK_ENV:development SESSION_SECRET: environments: -RACK_ENV=development -SESSION_SECRET123456123456 env_file 从一个文件中加入环境变量,该文件可以是一个单独的值或者一张列表,在environment中指定的环境变量将会重写这些值 env_file: -.envRACK_ENV:development1234512345 net 网络模式,可以在docker客户端的--net参数中指定这些值 net:"bridge"net:"none"net:"container:[nameorid]"net:"host"12341234 dns 自定义DNS服务,可以是一个单独的值或者一张列表 dns:8.8.8.8 dns: -8.8.8.8-9.9.9.912341234 cap_add,cap_drop 加入或者去掉容器能力,查看man 7 capabilities可以有一张完整的列表 cap_add: -ALLcap_drop: -NET_ADMIN-SYS_ADMIN123456123456 dns_search 自定义DNS搜索范围,可以是单独的值或者一张列表 dns_search:example.comdns_search: -dc1.example.com -dc2.example.com12341234 working_dir,entrypoint,user,hostname,domainname,mem_limit,privileged,restart,stdin_open,tty,cpu_shares 上述的每一个都只是一个单独的值,和docker run中对应的参数是一样的 cpu_shares:73working_dir:/codeentrypoint:/code/entrypoint.shuser:postgresqlhostname:foodomainname:foo.commem_limit:1000000000privileged:truerestart:alwaysstdin_open:truetty:true1234567891011121314151612345678910111213141516 六、Compose环境变量说明 环境变量已经不再是用来连接服务的推荐方法了,相反,应该使用链接名称(默认情况下是链接服务的名称)作为主机名称来连接,这可以查看docker-compose.yml的更多细节 Compose使用Docker links来暴露服务的容器给其他的。每一个链接的容器都使用了一组环境变量,这每一组环境变量都是以容器名称的大写字母开头的 要查看服务可用的环境变量,运行docker-compose run SERVICE env name_PORT 完整URL,如:DB_PORT=tcp//172.17.0.5:5432 name_PORT_num_protocol 完整URL,如:DB_PORT_5432_TCP=tcp://172.17.0.5:5432 name_PORT_num_protocol_ADDR 容器的IP地址,如:DB_PORT_5432_TCP_ADDR=172.17.0.5 name_PORT_num_protocol_PORT 暴露的端口号,如:DB_PORT_5432_TCP_PORT=5432 name_PORT_num_protocol_PROTO 协议(tcp或者udp),如:DB_PORT_5432_TCP_PROTO=tcp name_NAME 完全合格的容器名称,如:DB_1_NAME=/myapp_web_1/myapp_db_1 原文链接:http://blog.csdn.net/zhiaini06/article/details/45287663

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

全栈看到的技术债务

版权声明:本文为半吊子子全栈工匠(wireless_com,同公众号)原创文章,未经允许不得转载。 https://blog.csdn.net/wireless_com/article/details/78454200 “软件和大教堂类似,都是先构建,然后祈祷”。————Earl Everett 关于技术债务的讨论时而蔓延时而消退,技术债务仿佛是个筐,什么东西都可以往里装,然而当我们企图倒光筐里东西的时候,却发现每人看到的东西都不一样,甚至有时候都数不清里面都有些什么。 作为一个半吊子全栈工匠,试图从一个老码农的视角审视一下技术债务。 一个比喻导致的分歧 技术债务是由敏捷先驱 Ward Cunningham(https://en.wikipedia.org/wiki/Ward_Cunningham)在1992年的一个报告中的一个比喻,大意是做了错误的或不理想的技术决策所导致的债务。由于是个比喻,所以产生了每个人眼中的哈姆雷特。 首先,Steve McConnell将技术债务分为两类:无意的和有意的。 1)无意产生的技术债务:由于缺乏经验而编写了低质量的代码。 2)有意产生的技术债务:根据当前情况进行设计选型,可能很快就能解决当前的问题,有时会变得拙劣。 作为《重构》一书的作者,Martin Fowler认为技术债务产生的利息是指由于鲁莽的设计决策导致需要在未来的研发中付出更多。面对技术债务,可以持续付利,也可以通过重构一次付清。代码中的坏味道也是技术债务,是一种不计后果的债务,会让问题变得更加严重,进而将技术债务划分为4个象限: Bob大叔则认为坏味道并非技术债务。技术债务的评价标准是真实的项目约束,这些约束是风险和好处并存的。坏味道是由懒惰和外行导致的,总是意味着损失。技术债务要求牢记保持代码的整洁,就好像一个人在背负巨大的抵押债务时需要时刻保持警醒一样。 关于技术债务的更多定义讨论参见http://wiki.c2.com/?TechnicalDebt。 技术债务的现象与类型 将一个比喻作为定义是不严谨的,而且很多人对定义的重要性并不是很关注。于是,可以从现象和构成的角度来看待技术债务。 如何认识无意产生的技术债务呢?当出现如下现象的时候,说明技术债务开始累积: 1)类似的代码在不同的项目/产品间迁移 2)代码已经稳定但回归测试的陈本在增加 3)每一次交付的成本逐渐增加 4)类似feature 开发的周期开始变长 5)在既有代码上开发,还不如推到了重来 技术债务的类型是由产生的原因和方式来定义的,其中一种分类如下: 战略债务:为了战略利益(例如首次上市)故意为之,并长期存在。 战术债务:在知情的情况下为了快速收益而产生,适用于短期。 疏忽债务:在不知情的情况由于缺乏知识和意识而产生。 增量债务:定期不慎产生的而导致增量债务。 技术债务包括那些现在选择不做的内部事务,但会影响未来的开发工作,例如延迟重构。不包括推迟的功能实现,除非交付功能对客户来说“足够好”的情况下,但不满足某些标准(例如,UI元素不完全符合某些UI标准)。 技术债务的组成 技术债务是多方面的,涉及技术的多个维度,这也是每人眼中都有着自己的哈姆雷特的一个重要原因。 现罗列自己经历或者看到过的一些方面: 代码债务:代码重复、违反静态分析工具和代码异味等。 设计债务:设计异味、违反设计规则等 架构债务:违反架构规则,对非功能性约束认知不足等。 测试债务:缺乏测试、测试覆盖面不足和不正确的测试设计等。 质量债务:缺乏稳定性和健壮性的技术验证,QA的自动化不足等 配置债务:版本控制的模糊,环境参数的混淆等 平台债务:平台经验的匮乏,云服务融合与应用不足等 文档债务:存在重大问题的文档、缺乏文档和文档过期等。 针对每一类技术债务,都有着相应的原则和处理方法,有时间再仔细展开。 技术债务的度量之痛 No measurement,No management。令人遗憾的是,很难有相关工具或者框架来提供量化数据对其进行准确而完整的分析。 技术债务并没有一个普遍接受的范围定义,甚至认为已知的bug也构成了技术债务。当前可用的技术债务量化工具仅仅关注几个维度,比如代码债务和一定程度的设计债务和测试债务。对于同其它维度相关的问题,比如架构债务或文档债务,这类工具并没有提供全面的检测支持。实际上,已支持的维度的全面性也是有问题的。 一般,技术债务的量化工具通常会转换成偿还这些债务所需的工作量,而工作量会随问题的严重性、范围、平台、技能等的变化而不同。这样所产生的估计值与实际情况大不相符,最多只是一种近似,而不是准确的结论。 任何类型的债务都会包含本金和利息,而当前的一些所谓的量化工具都关注与技术债务相关的本金,而忽视了利息部分,从而并不可信。利息部分关系到了理解的难度等因素,越发难以准确地量化。 技术债务很好地充当了沟通拙劣设计结果和持续重构需求的隐喻。但试图测量和量化技术债务时,变得同准确测量软件生产力或者量化软件质量一样困难! 技术债务的现金衡量 当然,技术债务的货币化有助于了解技术债务的严重程度,提供了一种跟踪技术债务偿还进度的方法。不过,需要谨慎对待这些成本和工作量估计。 一旦有了与技术债务直接相关的金钱数目,关于软件的多种复杂而麻烦的问题就可能得到答案。Israel Gat提出:除非对于技术债务有一个量化的账单,否则团队都会忽略其重要性。他提到了以金钱方式计算技术债务的需要,以金钱方式计算技术债务有如下好处: 能够告诉团队何时停止开发,开始重构。团队进入重构过程,除非债务得以偿还,否则不加入任何功能。 客户对于软件的风险得以了解。 VC们可以以此判断向某项软件产品中投入资金是否理智。 有助于判断软件的支付能力,判断在重构和重写这二者之间做出选择 有哪些有效的方式可以用来将技术债务以金钱衡量呢? Sonar中的技术债务插件是一种方式。在Sonar的站点上,已经有了对于项目的技术债务分析。要计算成本,首先要使用下面的方式找出债务: 债务(人/天)=修复重复部分的成本 +修复违规的成本 +为公共API做注释的成本 +修复未发现的复杂性的成本 +带入低于阈值复杂性的成本 +在包的层面上切断生命周期的成本 对于每个小时的成本有个默认值,例如人工200块每小时。与之类似,就可以做出各种情况的现金分析,并可以计算出技术债务的总和。 因此,以金钱方式计算技术债务能够深入理解与软件相关的潜在成本。对于所有希望监控技术债务成本并将其保持在一定限额内的敏捷团队来说,这很关键。 直面技术债务 面对已知的技术债务,普遍的经验是防止技术债务的积聚,以及有计划地偿还技术债务。 防止技术债务产生的主要方法是了解开发团队存在的技术债务。开发团队必须了解技术债务,它的各种方面和类型,以及债务对他们的项目的影响。他们必须具备完善的设施与代码质量的概念,干净的编码习惯、设计嗅觉、以及如何重构它们。合适的流程可以帮助开发团队避免技术债务积累,例如代码、设计、架构和测试的审查等。然而,这些流程必须是务实的,否则事与愿违。 对于偿还技术债务而言,首先同样是识别并记录现有债务,优先处理异味,在每个迭代中分期偿还债务。如果一个研发团队的以团队交付功能的数量或修复bug的数量为考核标准,那么团队将只专注于增加功能和修复bug。但实际上,做好它和完成它同样重要。同时,留意可能出现的大规模债务偿还,属于不同维度的债务实例相互影响。在某些情况下,即使债务很高,也不值得偿还技术债务。这些情况包括原型、概念实现和即将废弃的产品所产生的技术债务。此外,如果计划为一个传统项目迁移到一个新的技术、平台或架构,不偿还的技术债务是明智的,因为相关的债务可以在迁移过程中解决的。 管理他人的技术债务 由于不提倡重复造轮子,我们所使用的第三方软件和开源软件产生的技术债务同样会对我们造成影响。它们的Bug会成为我们的Bug,安全漏洞也会成为我们的安全漏洞,错误决定会成为了我们的错误决定。 我们所使用其他软件的代码量可能会非常大,由此产生的技术债务也可能大,甚至超过自己所编写的代码量。根据Sonatype的一项调查显示,80%的Java应用都是由开源组件与框架装配起来的,一个大型系统甚至会使用30多个不同的库或组件。 要想了解这种债务问题的严重性,需要审查代码中使用了哪些第三方开源包与依赖。一旦清楚了其他人的软件可能会造成的影响,就需要评估由此所带来的风险和问题了,并需要紧密追踪这些软件的补丁、升级信息及Bug报告等,这确实不太容易。 知道问题的严重性是一方面,修复问题则是另一方面。对最新的发布打补丁并非易事。最好能在外围打补丁,因为补丁并非总是适合于我们所使用软件的方式:Apache、Tomcat、Web Service库、客户端组件等。我们需要更加谨慎地管理这些后端代码。如果每当出现一个Linux补丁时需要立刻打上,那就说明架构可能有些问题。 升级更是一个大问题了,升级到最新的OS、RDBMS、VM等都涉及到很高的代价与风险。虽然可以通过升级的方式获得在可伸缩性、管理性及新特性等方面的一些优势,不过升级项目还需要更加关注一些潜在的问题:功能回归、兼容性问题、操作流程的变化、对其他系统的依赖变化等等。升级之后,大多数软件会变得更大而不是更好,也许会加入很多新特性,不过可能压根就用不上这些特性,这也意味着需要花更多的时间进行安装、配置和测试。我们需要搞清楚变化的地方,重新进行测试和调优。 技术债务的价值 把技术债务和金融债务来类比,贷款就会产生债务,如果定期还款,那么债务是可接受的,不会产生进一步的问题。但是,如果他不还款,就会以利息作为惩罚,并随着不还款次数的增加而增加。如果很长一段时间不能支付任何款项,那么应计利息使得债务更难以偿还。 同样地,当我们采用一个非最优或次优技术决定,就引入了技术债务。在很长一段时间未偿还累计的技术债务的情况下,软件越来越难以改变,在极端情况下,软件产品变得在技术上已经破产,往往会导致项目终止。利息有复合的性质:开发团队越是忽略或推迟它,随着时间的推移,债务变得越大。因此,是利息使得技术债务成为一个显著问题。 我们谈及技术债务的时候往往都清楚它的种种弊端,却缺乏主动使用技术债务的勇气。 债务具有着现金价值,在金融领域中,货币杠杆的作用显著。 对企业而言,现金流更是关键因素, 所以,没有必要谈债色变,没有债的企业未必都是一流的好企业。 合理使用技术债务,在这个快节奏的时代,我们有责任迅速为客户提供价值。在这种追求中,有各种情况团队必须选择快速或者肮脏的解决方案。需要注意的是,在这种情况下,需要我们以勤奋和务实的态度处理技术债务。

资源下载

更多资源
Mario

Mario

马里奥是站在游戏界顶峰的超人气多面角色。马里奥靠吃蘑菇成长,特征是大鼻子、头戴帽子、身穿背带裤,还留着胡子。与他的双胞胎兄弟路易基一起,长年担任任天堂的招牌角色。

腾讯云软件源

腾讯云软件源

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

Nacos

Nacos

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

Spring

Spring

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

用户登录
用户注册