关于string:JavaString的常用方法总结

Java-String的罕用办法总结: 一、String类 String类在java.lang包中,java应用String类创立一个字符串变量,字符串变量属于对象。java把String类申明的final类,不能继承。String类对象创立后不能批改,由0或多个字符组成,蕴含在一对双引号之间。 二、String类构造方法 1、public String() 无参构造方法,用来创立空字符串的String对象。 String str1=new String(); String str2=new String("asdf"); 2、public String(String value) String str2=new String("asdf"); 3、public String(char[]value) char[]value={'a','b','c','d'}; String str4=new String(value); 4、public String(char chars[],int startIndex,int numChars) char[]value={'a','b','c','d'}; String str5=new String(value,1,2); 5、public String(byte[]values) byte[]strb=new byte[]{65,66}; String str6=new String(strb); 三、String类罕用办法 1、public char charAt(int index) 参数 index--字符的索引。 ...

April 3, 2023 · 2 min · jiezi

关于string:去掉String属性的最后一个逗号

也能够是其余符号,有这种问题的时候应该思考先改存储的中央(能够按制订字符串拆分成数组,长度大于1的时候才加分隔符,否则开端就不必增加了) Boolean isNotEmpty = StringUtils.isNotBlank(this.version);Boolean lastIndex = false;if(isNotEmpty){ lastIndex = ",".equals(this.version.substring(version.length()-1));} if(isNotEmpty && lastIndex){ return this.version.substring(0,version.length()-1);}else{ return this.version;}

August 17, 2021 · 1 min · jiezi

关于string:String类型和基本数据类的转化

一、由根本数据型态转换成StringString 类别中曾经提供了将根本数据型态转换成 String 的 static 办法 ,也就是 String.valueOf() 这个参数多载的办法 有以下几种 (1)String.valueOf(boolean b) : 将 boolean 变量 b 转换成字符串 (2)String.valueOf(char c) : 将 char 变量 c 转换成字符串 (3)String.valueOf(char[] data) : 将 char 数组 data 转换成字符串 (4)String.valueOf(char[] data, int offset, int count) : 将 char 数组 data 中 由 data[offset] 开始取 count 个元素 转换成字符串 (5)String.valueOf(double d) : 将 double 变量 d 转换成字符串 (6)String.valueOf(float f) : 将 float 变量 f 转换成字符串 (7)String.valueOf(int i) : 将 int 变量 i 转换成字符串 (8)String.valueOf(long l) : 将 long 变量 l 转换成字符串 (9)String.valueOf(Object obj) : 将 obj 对象转换成 字符串, 等于 obj.toString() ...

July 16, 2021 · 1 min · jiezi

关于string:前端笔记-字符串

所有罕用字符都是2字节的代码 反斜线 console.log(``) // ''字符串办法 s[i] => s.charAt(i)// 定位s.indexOf(str,index) // 从index开始在s中查找strs.lastIndexOf(str,index) // 从indx开始从后往前在s中查找strs.includes(str,index) // 从indx开始查找s中是否蕴含strs.startsWith(str) // s是否以str开始s.endsWith(str) // s是否以str完结// 获取子字符串s.slice(start,end)  // 不包含end。能够为负值。s.slice()相当于复制ss.substring(start,end) // 不包含end。负值=0。容许start>end,主动换地位s.substr(start,length) // 从start开始长度为length的字符串。能够为负值// 字符串比拟小写 > 大写s.codePointAt(index) // 返回s中在index地位的UTF-16编码 => s.charCodeAt(index)String.fromCodePoint(num) // 返回UTF-16编码代表的数字 => String.fromCharCode(num)s1.localeCompare(s2) // 返回一个num,示意s1是否在s2之前// 其余s.trim() // 删除s前后空格s.repeat(num) // 反复字符串num次按位NOT ~:它将数字转换为 32-bit 整数(如果存在小数局部,则删除小数局部),而后对其二进制示意模式中的所有位均取反。 ~n = -(n+1)

April 10, 2021 · 1 min · jiezi

关于string:go的stringbyte和rune类型

rune是int32的别名类型,一个值就代表一个Unicode字符。byte是uint8的别名类型,一个值就代表一个ASCII码的一个字符。rune类型的值在底层都是由一个 UTF-8 编码值来表白的。 理解下什么是Unicode字符和ASCII码:1、简略了解,咱们平时接触到的中英日文,或者复合字符,都是Unicode字符。比方,'G'、'o'、'爱'、'好'、'者'就是一个个Unicode字符。2、字符在计算机中存储时须要应用二进制数来示意。所以人们定义了一张表,将咱们用到的字符用一个二进制数值示意。这就是ASCII码表的由来。 UTF-8 编码方案会把一个 Unicode 字符编码为一个长度在 1\~4 以内的字节序列。所以,一个rune类型值代表了1\~4个长度的byte数组。 案例: func main() { str := "Go爱好者" fmt.Printf("The string: %q\n", str) fmt.Printf(" => runes(char): %q\n", []rune(str)) fmt.Printf(" => runes(hex): %x\n", []rune(str)) fmt.Printf(" => bytes(hex): [% x]\n", []byte(str))}The string: "Go爱好者" => runes(char): ['G' 'o' '爱' '好' '者'] => runes(hex): [47 6f 7231 597d 8005] => bytes(hex): [47 6f e7 88 b1 e5 a5 bd e8 80 85]字符串值"Go爱好者"如果被转换为[]rune类型的值的话,其中的每一个字符(不论是英文还是中文)就都会独立成为一个rune类型的元素值。如打印出的第二行内容。又因为,每个rune类型的值在底层都是由一个 UTF-8 编码值来表白的,如第三行把每个字符的 UTF-8 编码值都拆成相应的字节序列,如第四行,因为一个中文字符的 UTF-8 编码值须要用三个字节来表白。 ...

February 20, 2021 · 1 min · jiezi

关于string:原来只想简单看一下String源码没想到整理了这么多知识点

不晓得大家有没有这样得经验,就是无心中点进去得一个业面,而后钻到外面浏览了良久,我就是这样得,明天无心中,ctrl+左键,就点进了string得源码,正好今天下午没啥事,就在外面看一下,没想到,下次缓过去,就是我共事拍我让我去吃饭,哈哈哈哈,不过益处就是,我这边也整顿了一些string类得知识点,也分享给大家,整顿得不好还望海涵文章首发集体公众号:Java架构师联盟,每日更新技术好文一、String类想要理解一个类,最好的方法就是看这个类的实现源代码,来看一下String类的源码:public final class String implements java.io.Serializable, Comparable<String>, CharSequence{/** The value is used for character storage. */private final char value[];/** The offset is the first index of the storage that is used. */private final int offset;/** The count is the number of characters in the String. */private final int count;/** Cache the hash code for the string */private int hash; // Default to 0/** use serialVersionUID from JDK 1.0.2 for interoperability */private static final long serialVersionUID = -6849794470754667710L;........}从下面能够看出几点:1)String类是final类,也即意味着String类不能被继承,并且它的成员办法都默认为final办法。在Java中,被final润饰的类是不容许被继承的,并且该类中的成员办法都默认为final办法。2)下面列举出了String类中所有的成员属性,从下面能够看出String类其实是通过char数组来保留字符串的。上面再持续看String类的一些办法实现:public String substring(int beginIndex, int endIndex) { ...

February 1, 2021 · 2 min · jiezi

关于string:String类的substring和lastIndexOf方法的使用

String类的substring()和lastIndexOf()办法的应用两个办法substring():返回字符串的子字符串;语法: public String substring(int beginIndex)public String substring(int beginIndex, int endIndex)beginIndex -- 起始索引(包含), 索引从 0 开始; endIndex -- 完结索引(不包含);lastIndexOf():返回指定字符/字符串在此字符串中最初一次呈现处的索引,如果此字符串中没有这样的字符,则返回 -1;语法: public int lastIndexOf(int ch)public int lastIndexOf(int ch, int fromIndex)public int lastIndexOf(String str)public int lastIndexOf(String str, int fromIndex)ch -- 字符;fromIndex -- 开始搜寻的索引地位,索引起始值为0;str -- 要搜寻的子字符串;举个栗子场景:上传文件时获取文件的后缀名;形容:前端传来文件时(.txt、.png、.jpg等),咱们为了避免重名,个别会在后盾对文件进行重命名,然而要保障不扭转文件的类型,因而要先拿到文件名后缀,而后再通过拼接操作对文件进行重命名; //获取文件名String filename = file.getOriginalFilename();//文件类型后缀String suffix = filename.substring(filename.lastIndexOf(".") + 1);//重命名文件String nickname = System.currentTimeMillis() + "." + suffix;参考链接:https://www.runoob.com/java/j...https://www.runoob.com/java/j...

January 5, 2021 · 1 min · jiezi

关于string:因为这7个C的坑整个团队加班一星期

摘要:近期踩到了一些比拟费解的C++的坑,可把咱们团队给坑惨了~~近期咱们团队进行版本品质加固时,踩到了一些比拟费解的C++的坑,特总结分享在此,供大家参考。 string的字符串拼接,导致coredump 该问题的外围点在于第9行,居然是能够编译通过,其起因是x+"-",会被转成char*,而后与to_string叠加导致BUG。 map的迭代器删除map要删除一个元素,通常通过erase()函数来实现,然而要留神,如果咱们传入了一个iterator作为erase的参数来删除以后迭代器所指向的元素,删除实现后iterator会生效,产生未定义行为。正确的应用办法应该是接管erase()的返回值,让iterator指向被删除元素的下一个元素或者end()。 for ( auto iter = m.begin(); iter != m.end(); iter++) { if (...) iter = m.erase(iter); } 然而上述代码依然有谬误,因为如果触发了删除,那么iter再下一轮循环时会指向下下个元素,所以正确的写法应该是: for ( auto iter = m.begin(); iter != m.end();) { if (...) { iter = m.erase(iter); continue ; } else { iter++; } } stringstream的性能问题stringstream的清空是clear之后,置空。stringstream在任何状况下都比snprintf慢。memset是个很慢的函数,宁愿新创建对象。上述测试后果是单线程,改成多线程,同样成立。str += “a”, 比 str =str+ “a” 效率高很多,后者会创立新对象。智能指针(shared_ptr)应用留神4.1尽量应用make_shared初始化进步性能 std::shared_ptr<Widget> spw(newWidget); 须要调配两次内存。每个std::shared_ptr都指向一个管制块,管制块蕴含被指向对象的援用计数以及其余货色。这个管制块的内存是在std::shared_ptr的构造函数中调配的。因而间接应用new,须要一块内存调配给Widget,还要一块内存调配给管制块 autospw = std::make_shared<Widget>(); 一次调配就足够了。这是因为std::make_shared申请一个独自的内存块来同时寄存Widget对象和管制块。这个优化缩小了程序的动态大小,因为代码只蕴含一次内存调配的调用,并且这会放慢代码的执行速度,因为内存只调配了一次。另外,应用std::make_shared打消了一些管制块须要记录的信息,这样潜在地缩小了程序的总内存占用。 异样平安 processWidget(std::shared_ptr<Widget>( new Widget), //潜在的资源泄露 computePriority()); 上述代码存在内存透露的危险,上述代码执行分为3个步骤: ...

November 3, 2020 · 1 min · jiezi

关于string:常见包装类ListSetHashMap见解

一. Integer与String1.1 Integer包装类,就是对根本数据类型的一个包装,赋予一些罕用的办法 比方类型装换等。 int的包装类,应用final润饰类,不可继承。 会主动装箱与主动拆箱。 特地留神:在-128到127之间,保护了一个数组(static初始化,服务器启动就存在。) 比拟值时,如果是这个区间,则默认从数组中拿出值来进行判断。 Integer a = 1;Integer b = 1;Integer c = 128;Integer d = 128;sout(a == b); // truesout(c == d); // false源码局部 private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } public static Integer valueOf(int i) { if (i >= IntegerCache.low && i <= IntegerCache.high) return IntegerCache.cache[i + (-IntegerCache.low)]; return new Integer(i); }1.2 String不可变长字符串。 ...

October 22, 2020 · 4 min · jiezi

关于string:初识String源码

前言最近打算开始来读一下JDK的局部源码,这次先从咱们平时用的最多的String类(JDK1.8)开始,本文次要会对以下几个办法的源码进行剖析: equalshashCodeequalsIgnoreCaseindexOfstartsWithconcatsubstringsplittrimcompareTo如果有不对的中央请多多指教,那么开始进入注释。 源码分析首先看下String类实现了哪些接口 public final class String implements java.io.Serializable, Comparable<String>, CharSequence {java.io.Serializable 这个序列化接口没有任何办法和域,仅用于标识序列化的语意。 Comparable<String> 这个接口只有一个compareTo(T 0)接口,用于对两个实例化对象比拟大小。 CharSequence 这个接口是一个只读的字符序列。包含length(), charAt(int index), subSequence(int start, int end)这几个API接口,值得一提的是,StringBuffer和StringBuild也是实现了该接口。 看一下两个次要变量: /** The value is used for character storage. */private final char value[];/** Cache the hash code for the string */private int hash; // Default to 0能够看到,value[]是存储String的内容的,即当应用String str = "abc";的时候,实质上,"abc"是存储在一个char类型的数组中的。 而hash是String实例化的hashcode的一个缓存。因为String常常被用于比拟,比方在HashMap中。如果每次进行比拟都从新计算hashcode的值的话,那无疑是比拟麻烦的,而保留一个hashcode的缓存无疑能优化这样的操作。 留神:这边有一个须要留神的点就是能够看到value数组是用final润饰的,也就是说不能再去指向其它的数组,然而数组的内容是能够扭转的,之所以说String不可变是因为其提供的API(比方replace等办法)都会给咱们返回一个新的String对象,并且咱们无奈去扭转数组的内容,这才是它不可变的起因。 equalsequals() 办法用于判断 Number 对象与办法的参数进是否相等String类重写了父类Object的equals办法,来看看源码实现: 首先会判断两个对象是否指向同一个地址,如果是的话则是同一个对象,间接返回true接着会应用instanceof判断指标对象是否是String类型或其子类的实例,如果不是的话则返回false接着会比拟两个String对象的char数组长度是否统一,如果不统一则返回false最初迭代顺次比拟两个char数组是否相等hashCodehashCode() 办法用于返回字符串的哈希码Hash算法就是一种将任意长度的消息压缩到某一固定长度的音讯摘要的函数。在Java中,所有的对象都有一个int hashCode()办法,用于返回hash码。 依据官网文档的定义:Object.hashCode() 函数用于这个函数用于将一个对象转换为其十六进制的地址。依据定义,如果2个对象雷同,则其hash码也应该雷同。如果重写了 equals() 办法,则原 hashCode() 办法也一并生效,所以也必须重写 hashCode() 办法。 ...

October 5, 2020 · 3 min · jiezi

关于string:String-的总结

String根本个性String : 字符串,应用一对""引起来示意。 String申明为final的。,不可被继承 String 实现了Serializeable接口:示意字符串反对序列化的。实现了Comparable 接口:示意String能够比拟大小 String在jdk8 及以前外部定义了final char[] value 用于存储字符串数据。jdk9 时改为byte[] String存储构造变更String应用byte[]加上编码标记,节约了内存空间 StringBuffer 和StringBuilder 都也有相应的改变。 String的根本个性String代表不可变的字符序列,即不可变性 当对字符串从新赋值时,须要重写指定内存区域赋值,不能应用原有的value进行赋值。当对现有的字符串进行间断操作时,也须要从新指定内存区域赋值,不能应用原有的value进行赋值当调用String的replace()办法批改指定字符或字符串时,也须要从新指定内存区域赋值,不能应用原有的value进行赋值通过字面量的形式(区别于new)给一个字符串赋值,此时的字符串值申明在字符串常量池中。字符串常量池中是不会存储雷同内容的字符串。 String的String Pool 是一个固定大小的Hashtable,默认值大小长度是1009,如果放进String Pool 的String十分多,就会造成Hash抵触,从而导致链表会很长,而链表长了后会造成的影响就是当调用String.intern时性能会大幅降落。 应用-XX:StringTableSize 可设置StringTable的长度 在jdk6中StringTable是固定的,就是1009 的长度,所以如果常量池中的字符串过多就会导致效率降落地很快。StringTableSize设置没有要求 在jdk7中,StringTable的长度默认值是60012 ,jdk8开始,1009是能够设置的最小值。 String 的内存调配Java6及之前,字符串常量池寄存在永恒代。 Java7 中Oracle的工程师对字符串池的逻辑做了很大的变动,行将字符串常量池的地位调整到Java堆内。 所有的字符串都保留在堆(Heap)中,就像其余一般对象一样,这样能够让你在进行调优利用时仅需调整堆大小就能够了。字符串常量池概念本来应用得比拟多,然而这个改变使得咱们有足够的理由让咱们重新考虑在java7 中应用String.intern()。Java8 元空间,字符串常量在堆。 String的基本操作Java语言标准里要求完全相同的字符串字面量,应该蕴含同样的Unicode字符序列(蕴含同一份码点序列的常量),并且必须是指向同一个String类实例。 字符串拼接操作常量与常量的拼接后果是在常量池,原理是编译期优化常量池中不会存在雷同内容的常量只有其中有一个是变量,后果就在堆中。变量拼接的原理是StringBuilder如果拼接的后果调用intern()办法,则被动将常量池中还没有的字符串对象放入池中,并返回对象地址。@Testpublic void test3(){ String s1 = "a"; String s2 = "b"; String s3 = "ab"; /* 如下的s1 + s2 的执行细节:(变量s是我长期定义的) ① StringBuilder s = new StringBuilder(); ② s.append("a") ③ s.append("b") ④ s.toString() --> 约等于 new String("ab") 补充:在jdk5.0之后应用的是StringBuilder,在jdk5.0之前应用的是StringBuffer *//*1. 字符串拼接操作不肯定应用的是StringBuilder! 如果拼接符号左右两边都是字符串常量或常量援用,则依然应用编译期优化,即非StringBuilder的形式。2. 针对于final润饰类、办法、根本数据类型、援用数据类型的量的构造时,能应用上final的时候倡议应用上。 *//** * 如何保障变量s指向的是字符串常量池中的数据呢? * 有两种形式: * 形式一: String s = "shkstart";//字面量定义的形式 * 形式二: 调用intern() * String s = new String("shkstart").intern(); * String s = new StringBuilder("shkstart").toString().intern(); * */public class StringIntern { public static void main(String[] args) { String s = new String("1"); s.intern();//调用此办法之前,字符串常量池中曾经存在了"1" String s2 = "1"; System.out.println(s == s2);//jdk6:false jdk7/8:false String s3 = new String("1") + new String("1");//s3变量记录的地址为:new String("11") //执行完上一行代码当前,字符串常量池中,是否存在"11"呢?答案:不存在!! s3.intern();//在字符串常量池中生成"11"。如何了解:jdk6:创立了一个新的对象"11",也就有新的地址。 // jdk7:此时常量中并没有创立"11",而是创立一个指向堆空间中new String("11")的地址 String s4 = "11";//s4变量记录的地址:应用的是上一行代码代码执行时,在常量池中生成的"11"的地址 System.out.println(s3 == s4);//jdk6:false jdk7/8:true }}public class StringIntern1 { public static void main(String[] args) { //StringIntern.java中练习的拓展: String s3 = new String("1") + new String("1");//new String("11") //执行完上一行代码当前,字符串常量池中,是否存在"11"呢?答案:不存在!! String s4 = "11";//在字符串常量池中生成对象"11" String s5 = s3.intern(); System.out.println(s3 == s4);//false System.out.println(s5 == s4);//true }}总结String 的intern()的应用Jdk1.6中,将这个字符串对象尝试放入串池 ...

August 29, 2020 · 1 min · jiezi

Java-字符串-split-踩坑记

1.1 split 的坑前几天在公司对通过 FTP 方式上传的数据文件按照事先规定的格式进行解析后入库,代码的大概实现思路是这样的:先使用流进行文件读取,对文件的每一行数据解析封装成一个个对象,然后进行入库操作。本以为很简单的一个操作,然后写完代码后自己测试发现对文件的每一行进行字符串分割的时候存在问题,在这里做个简单的记录总结。在 Java 中使用 split 方法对字符串进行分割是经常使用的方法,经常在一些文本处理、字符串分割的逻辑中,需要按照一定的分隔符进行分割拆解。这样的功能,大多数情况下我们都会使用 String 中的 split 方法。关于这个方法,稍不注意很容易踩坑。 (1)split 的参数是正则表达式首先一个常见的问题,就是忘记了 String 的 split 方法的参数不是普通的字符串,而是正则表达式,例如下面的这两种使用方式都达不到我们的预期: /** * @author mghio * @date: 2019-10-13 * @version: 1.0 * @description: Java 字符串 split 踩坑记 * @since JDK 1.8 */ public class JavaStringSplitTests { @Test public void testStringSplitRegexArg() { System.out.println(Arrays.toString("m.g.h.i.o".split("."))); System.out.println(Arrays.toString("m|g|h|i|o".split("|"))); } }<!-- more --> 以上代码的结果输出为: [][m, |, g, |, h, |, i, |, o]上面出错的原因是因为 . 和 | 都是正则表达式,应该用转义字符进行处理: ...

October 14, 2019 · 4 min · jiezi

记一次代码重构

单一职责功能单一功能单一是SRP最基本要求,也就是你一个类的功能职责要单一,这样内聚性才高。 比如,下面这个参数类,是用来查询网站Buyer信息的,按照SRP,里面就应该放置查询相关的Field就好了。 @Datapublic class BuyerInfoParam { // Required Param private Long buyerCompanyId; private Long buyerAccountId; private Long callerCompanyId; private Long callerAccountId; private String tenantId; private String bizCode; private String channel; //这个Channel在查询中不起任何作用,不应该放在这里}可是呢? 事实并不是这样,下面的三个参数其实查询时根本用不到,而是在组装查询结果的时候用到,这给我阅读代码带来了很大的困惑,因为我一直以为这个channel(客户来源渠道)是一个查询需要的一个重要信息。 那么如果和查询无关,为什么要把它放到查询param里面呢,问了才知道,只是为了组装查询结果时拿到数据而已。 所以我重构的时候,果断把查询没用到的参数从BuyerInfoParam中移除了,这样就消除了理解上的歧义。 Tips:不要为了图方便,而破坏SOLID原则,方便的后果就是代码腐化,看不懂,往后要付出的代价更高。 功能内聚在类的职责单一基础之上,我们还要识别出是不是有功能相似的类或者组件,如果有,是不是要整合起来,而不要让功能类似的代码散落在多处。 比如,代码中我们有一个TenantContext,而build这个Context统一是在ContextPreInterceptor中做的,其中Operator的值一开始只有crmId,但是随着业务的变化operator的值在不同的场景值会不一样,可能是aliId,也可能是accountId。 这样就需要其它id要转换成crmId的工作,重构前这个转换工作是分散在多个地方,不满足SRP。 //在BaseMtopServiceImpl中有crmId转换的逻辑 public String getCrmUserId(Long userId){ AccountInfoDO accountInfoDO = accountQueryTunnel.getAccountDetailByAccountId(userId.toString(), AccountTypeEnum.INTL.getType(), false); if(accountInfoDO != null){ return accountInfoDO.getCrmUserId(); } return StringUtils.EMPTY; } //在ContextUtilServiceImpl中有crmId转换的逻辑 public String getCrmUserIdByMemberSeq(String memberSeq) { if(StringUtil.isBlank(memberSeq)){ return null; } MemberMappingDO mappingDO = memberMappingQueryTunnel.getMappingByAccountId(Long.valueOf(memberSeq)); if(mappingDO == null || mappingDO.getAliId() == null){ return null; } }重构的做法是将build context的逻辑,包括前序的crmId的转换逻辑,全部收拢到ContextPreInterceptor,因为它的职责就是build context,这样代码的内聚性,可复用性和可读性都会好很多。 ...

August 7, 2019 · 4 min · jiezi

阿里云人脸识别公测使用说明

概述之前阿里云人脸识别只提供人脸检测,人脸属性及人脸对比三个API接口,关于这方面的介绍及使用细节,可以参考阿里云人脸识别使用流程简介,之前使用的服务地址为:dtplus-cn-shanghai.data.aliyuncs.com。目前新版本加入了1:N人脸查找的功能,新版本还处于公测阶段,服务地址:face.cn-shanghai.aliyuncs.com。下面主要介绍如何使用新版本的地址调用之前的三个API的功能。使用流程1、服务开通及1:N人脸识别使用阿里云人脸识别 1:N 使用简明示例 2、接口调用Code示例 import com.aliyuncs.CommonRequest;import com.aliyuncs.CommonResponse;import com.aliyuncs.DefaultAcsClient;import com.aliyuncs.exceptions.ClientException;import com.aliyuncs.http.MethodType;import com.aliyuncs.profile.DefaultProfile;public class CommomDemo { //DefaultProfile.getProfile的参数分别是地域,access_key_id, access_key_secret public static DefaultProfile profile = DefaultProfile.getProfile("cn-shanghai", "********", "********"); public static DefaultAcsClient client = new DefaultAcsClient(profile); public static void main(String[] args) { String imageUrl_1 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1559655604341&di=3d6995f6dee1c4795d1827e754a00452&imgtype=0&src=http%3A%2F%2Fimg0.ph.126.net%2F90u9atgu46nnziAm1NMAGw%3D%3D%2F6631853916514183512.jpg"; String imageUrl_2 = "https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1559655604338&di=ee3d8fb39f6e14a21852a4ac3f2c5a14&imgtype=0&src=http%3A%2F%2Fc4.haibao.cn%2Fimg%2F600_0_100_0%2F1473652712.0005%2F87c7805c10e60e9a6db94f86d6014de8.jpg"; // 人类检测定位 DetectFace(imageUrl_1); // 人类属性识别 GetFaceAttribute(imageUrl_1); // 人脸对比 VerifyFace(imageUrl_1,imageUrl_2); } /** * DetectFace API 人脸检测定位 * * @param imageUrl 检测人脸图片的URL */ public static void DetectFace(String imageUrl) { CommonRequest request = new CommonRequest(); request.setMethod(MethodType.POST); request.setDomain("face.cn-shanghai.aliyuncs.com"); request.setVersion("2018-12-03"); request.setAction("DetectFace"); request.putBodyParameter("ImageUrl", imageUrl);// request.putBodyParameter("Content", "/9j/4AAQSkZJRgABA..."); //检测图片的内容,Base64编码 CommonResponse response = null; try { response = client.getCommonResponse(request); } catch (ClientException e) { e.printStackTrace(); } System.out.println(response.getData()); } /** * GetFaceAttribute API 人脸属性识别 * * @param imageUrl 检测人脸图片的URL */ public static void GetFaceAttribute(String imageUrl) { CommonRequest request = new CommonRequest(); request.setMethod(MethodType.POST); request.setDomain("face.cn-shanghai.aliyuncs.com"); request.setVersion("2018-12-03"); request.setAction("GetFaceAttribute"); request.putBodyParameter("ImageUrl", imageUrl);// request.putBodyParameter("Content", "/9j/4AAQSkZJRgABA..."); //检测图片的内容,Base64编码 CommonResponse response = null; try { response = client.getCommonResponse(request); } catch (ClientException e) { e.printStackTrace(); } System.out.println(response.getData()); } /** * VerifyFace API 人脸比对 * * @param imageUrl_1 对比人脸图片1 * @param imageUrl_2 对比人脸图片2 */ public static void VerifyFace(String imageUrl_1, String imageUrl_2) { CommonRequest request = new CommonRequest(); request.setMethod(MethodType.POST); request.setDomain("face.cn-shanghai.aliyuncs.com"); request.setVersion("2018-12-03"); request.setAction("VerifyFace"); request.putBodyParameter("ImageUrl1", imageUrl_1); request.putBodyParameter("ImageUrl2", imageUrl_2);// request.putBodyParameter("Content", "/9j/4AAQSkZJRgABA..."); //检测图片的内容,Base64编码 CommonResponse response = null; try { response = client.getCommonResponse(request); } catch (ClientException e) { e.printStackTrace(); } System.out.println(response.getData()); }}3、测试结果 ...

July 8, 2019 · 1 min · jiezi

面试题寻找一个字符串中出现次数最多的字符以及出现的次数

要求编写代码实现:寻找一个字符串中出现次数最多的字符以及出现的次数。 解法一:用删除法实现 (挺巧妙的一种)public class FindTheMostAppearChar { public static void main(String[] args) { deleteMethodToAchieve(); } /** * 用删除法实现 (挺巧妙的) * 解题思路:每次取出字符串的第一个字符,将字符串中与第一个字符相同的字符全部删掉, * 然后通过计算删除前后字符串的长度来确定该字符在字符串出现的次数,最终比较出出现次数最多的字符 */ public static void deleteMethodToAchieve() { Scanner scanner = new Scanner(System.in); String string = scanner.nextLine().trim(); scanner.close(); int max_length = 0; String max_str = ""; while (string.length() > 0) { String firstChar = string.substring(0,1); int length = string.length(); string = string.replaceAll(firstChar, ""); if (length - string.length() > max_length) { max_length = length - string.length(); max_str = firstChar; } } System.out.println("出现次数最多的字符是:" + max_str + ",出现的次数:" + max_length); }}解法二:用查找法实现public class FindTheMostAppearChar { public static void main(String[] args) { hashMapMethodToAchieve(); } /** * 用字符数组查找法实现 * 解题思路:先将字符串拆分成字符数组,然后转存到 HashMap 集合中, * 该集合的key为字符串中出现的字符,value为对应字符串出现的次数。 * 最后只需要在HashMap集合中找到Value值最大的key即可。 */ public static void hashMapMethodToAchieve() { Scanner scanner = new Scanner(System.in); String string = scanner.nextLine().trim(); scanner.close(); // 将字符串转换成字符数组 char[] arr = string.toCharArray(); Map<Character, Integer> map = new HashMap<>(); // key为出现的字符,value 为该字符出现的次数,将字符数组转存在 HashMap 中 if (arr != null && arr.length > 0) { for (int i = 0; i < arr.length; i++) { if (map.get(arr[i]) != null) { // 若不为空,说明已经存在相同的字符,则 value 值在原来的基础上加1 map.put(arr[i],map.get(arr[i]) + 1); } else { map.put(arr[i], 1); } } } // 查找出出现次数最多的字符以及出现的次数也有多种写法 FindTheMostCharByMap(map); // 查找写法一:用 Iterator 遍历 Map 来查找 // FindTheMostCharByMapEntry(map); // 查找写法二:用 Map.Entry 提高效率 // FindTheMostCharByForLoop(map, arr); // 查找写法三:直接用 for 循环来遍历查找 } // 查找写法一:用 Iterator 遍历 Map 来查找 public statice void FindTheMostCharByMap(Map<Character, Integer> map) { Set<Character> keys = map.keySet(); // 获取所有的key Iterator iterator = keys.iterator(); // 实例化 Iterator Character maxKey = (Character) iterator.next(); //定义第一个为最大的value和对应的key int maxValue = map.get(maxKey); while (iterator.hasNext()) { Character temp = (Character) iterator.next(); if (maxValue < map.get(temp)) { maxKey = temp; maxValue = map.get(temp); } } System.out.println("出现次数最多的字符是:" + maxKey + ", 出现的次数:" + maxValue); } // 查找写法二:用 Map.Entry 提高效率 public static void FindTheMostCharByMapEntry(Map<Character, Integer> map) { Iterator iterator = map.entrySet().iterator(); Map.Entry entry = (Map.Entry) iterator.next(); char maxKey = (char) entry.getKey(); // 获取key int maxValue = (int) entry.getValue(); // 获取value while (iterator.hasNext()) { entry = (Map.Entry) iterator.next(); char tempKey = (char) entry.getKey(); int tempValue = (int) entry.getValue(); if (maxValue < tempValue) { maxKey = tempKey; maxValue = tempValue; } } System.out.println("出现次数最多的字符是:" + maxKey + ", 出现的次数:" + maxValue); } // 查找写法三:直接用 for 循环来遍历查找 public static void FindTheMostCharByForLoop(Map<Character, Integer> map, char[] arr) { int maxValue = map.get(arr[0]); char maxKey = ' '; for (int i = 0; i < arr.length; i++) { if (maxValue < map.get(arr[i])) { maxValue = map.get(arr[i]); maxKey = arr[i]; } } System.out.println("出现次数最多的字符是:" + maxKey + ", 出现的次数:" + maxValue); } }解法三:用排序法实现public class FindTheMostAppearChar { public static void main(String[] args) { sortMethodToAchieve(); } /** * 用排序法实现 * 解题思路:先将字符串转换成字符数组,然后对字符数组进行排序, * 统计每个字符重复出现的次数,最后比较得出出现次数最多的字符以及出现次数 */ public static void sortMethodToAchieve() { Scanner scanner = new Scanner(System.in); String string = scanner.nextLine().trim(); scanner.close(); char[] arr = string.toCharArray(); Arrays.sort(arr); // 对数组进行排序 char maxValue = 'a'; // 记录出现次数最多的元素 int maxCount = 0; // 记录出现次数 int count = 1; for (int i = 0; i < arr.length - 1; i++) { if (arr[i] == arr[i+1]) { count++; } if (arr[i] != arr[i+1]) { if (count > maxCount) { maxCount = count; maxValue = arr[i]; } count = 1; } } System.out.println("出现次数最多的字符是:" + maxValue + ", 出现的次数:" + maxCount); }}

June 20, 2019 · 3 min · jiezi

重磅开源AOP-for-Flutter开发利器AspectD

https://github.com/alibaba-flutter/aspectd 问题背景随着Flutter这一框架的快速发展,有越来越多的业务开始使用Flutter来重构或新建其产品。但在我们的实践过程中发现,一方面Flutter开发效率高,性能优异,跨平台表现好,另一方面Flutter也面临着插件,基础能力,底层框架缺失或者不完善等问题。 举个栗子,我们在实现一个自动化录制回放的过程中发现,需要去修改Flutter框架(Dart层面)的代码才能够满足要求,这就会有了对框架的侵入性。要解决这种侵入性的问题,更好地减少迭代过程中的维护成本,我们考虑的首要方案即面向切面编程。 那么如何解决AOP for Flutter这个问题呢?本文将重点介绍一个闲鱼技术团队开发的针对Dart的AOP编程框架AspectD。 AspectD:面向Dart的AOP框架AOP能力究竟是运行时还是编译时支持依赖于语言本身的特点。举例来说在iOS中,Objective C本身提供了强大的运行时和动态性使得运行期AOP简单易用。在Android下,Java语言的特点不仅可以实现类似AspectJ这样的基于字节码修改的编译期静态代理,也可以实现Spring AOP这样的基于运行时增强的运行期动态代理。那么Dart呢?一来Dart的反射支持很弱,只支持了检查(Introspection),不支持修改(Modification);其次Flutter为了包大小,健壮性等的原因禁止了反射。 因此,我们设计实现了基于编译期修改的AOP方案AspectD。 设计详图 典型的AOP场景下列AspectD代码说明了一个典型的AOP使用场景: aop.dartimport 'package:example/main.dart' as app;import 'aop_impl.dart';void main()=> app.main();aop_impl.dartimport 'package:aspectd/aspectd.dart';@Aspect()@pragma("vm:entry-point")class ExecuteDemo { @pragma("vm:entry-point") ExecuteDemo(); @Execute("package:example/main.dart", "_MyHomePageState", "-_incrementCounter") @pragma("vm:entry-point") void _incrementCounter(PointCut pointcut) { pointcut.proceed(); print('KWLM called!'); }}面向开发者的API设计PointCut的设计 @Call("package:app/calculator.dart","Calculator","-getCurTime")PointCut需要完备表征以怎么样的方式(Call/Execute等),向哪个Library,哪个类(Library Method的时候此项为空),哪个方法来添加AOP逻辑。PointCut的数据结构: @pragma('vm:entry-point')class PointCut { final Map<dynamic, dynamic> sourceInfos; final Object target; final String function; final String stubId; final List<dynamic> positionalParams; final Map<dynamic, dynamic> namedParams; @pragma('vm:entry-point') PointCut(this.sourceInfos, this.target, this.function, this.stubId,this.positionalParams, this.namedParams); @pragma('vm:entry-point') Object proceed(){ return null; }}其中包含了源代码信息(如库名,文件名,行号等),方法调用对象,函数名,参数信息等。请注意这里的@pragma('vm:entry-point')注解,其核心逻辑在于Tree-Shaking。在AOT(ahead of time)编译下,如果不能被应用主入口(main)最终可能调到,那么将被视为无用代码而丢弃。AOP代码因为其注入逻辑的无侵入性,显然是不会被main调到的,因此需要此注解告诉编译器不要丢弃这段逻辑。此处的proceed方法,类似AspectJ中的ProceedingJoinPoint.proceed()方法,调用pointcut.proceed()方法即可实现对原始逻辑的调用。原始定义中的proceed方法体只是个空壳,其内容将会被在运行时动态生成。 ...

June 19, 2019 · 2 min · jiezi

Spark内置图像数据源初探

概述在Apache Spark 2.4中引入了一个新的内置数据源, 图像数据源.用户可以通过DataFrame API加载指定目录的中图像文件,生成一个DataFrame对象.通过该DataFrame对象,用户可以对图像数据进行简单的处理,然后使用MLlib进行特定的训练和分类计算.    本文将介绍图像数据源的实现细节和使用方法. 简单使用先通过一个例子来简单的了解下图像数据源使用方法. 本例设定有一组图像文件存放在阿里云的OSS上, 需要对这组图像加水印,并压缩存储到parquet文件中. 废话不说,先上代码: // 为了突出重点,代码简化图像格式相关的处理逻辑 def main(args: Array[String]): Unit = { val conf = new SparkConf().setMaster("local[*]") val spark = SparkSession.builder() .config(conf) .getOrCreate() val imageDF = spark.read.format("image").load("oss://<bucket>/path/to/src/dir") imageDF.select("image.origin", "image.width", "image.height", "image.nChannels", "image.mode", "image.data") .map(row => { val origin = row.getAs[String]("origin") val width = row.getAs[Int]("width") val height = row.getAs[Int]("height") val mode = row.getAs[Int]("mode") val nChannels = row.getAs[Int]("nChannels") val data = row.getAs[Array[Byte]]("data") Row(Row(origin, height, width, nChannels, mode, markWithText(width, height, BufferedImage.TYPE_3BYTE_BGR, data, "EMR"))) }).write.format("parquet").save("oss://<bucket>/path/to/dst/dir") } def markWithText(width: Int, height: Int, imageType: Int, data: Array[Byte], text: String): Array[Byte] = { val image = new BufferedImage(width, height, imageType) val raster = image.getData.asInstanceOf[WritableRaster] val pixels = data.map(_.toInt) raster.setPixels(0, 0, width, height, pixels) image.setData(raster) val buffImg = new BufferedImage(width, height, imageType) val g = buffImg.createGraphics g.drawImage(image, 0, 0, null) g.setColor(Color.red) g.setFont(new Font("宋体", Font.BOLD, 30)) g.drawString(text, width/2, height/2) g.dispose() val buffer = new ByteArrayOutputStream ImageIO.write(buffImg, "JPG", buffer) buffer.toByteArray }从生成的parquet文件中抽取一条图像二进制数据,保存为本地jpg,效果如下: ...

June 17, 2019 · 3 min · jiezi

String-的属性和方法

属性 length字符串实例的 length 属性返回字符串的长度。 let a = "aaaa";a.length // 4跟数组不同的是,给 length 属性赋值,不会改变原字符串的值。 方法静态方法1. String.fromCharCode()String.fromCharCode() 方法的参数是一个或多个数值,代表 Unicode 码点,返回值是这些码点组成的字符串。 String.fromCharCode(); // ""String.fromCharCode(97); // "a"String.fromCharCode(104, 101, 108, 108, 111); // "hello"注意,该方法不支持 Unicode 码点大于0xFFFF的字符,即传入的参数不能大于0xFFFF(即十进制的 65535)。 2. String.fromCodePoint()String.fromCodePoint() 方法也是将Unicode 码点转为字符串,但可以识别大于0xFFFF的字符,弥补了String.fromCharCode() 方法的不足。 String.fromCodePoint(0x20BB7) // "????"该方法的作用,与 codePointAt() 方法相反。需要注意的是 fromCodePoint() 方法定义在 String 对象上,而 codePointAt() 方法定义在字符串的实例对象上。 实例方法1. String.prototype.charAt()charAt() 方法返回指定位置的字符,参数是从 0 开始编号的位置。 let s = 'abc's.charAt(1) // "b"s.charAt(s.length - 1) // "c"这个方法完全可以用数组下标替代。 'abc'.charAt(1) // "b"'abc'[1] // "b"如果参数为负数,或大于等于字符串的长度,charAt 返回空字符串。 ...

May 10, 2019 · 4 min · jiezi

javascript实现ArrayBuffer转字符串微信小程序蓝牙数据转换

/** * ArrayBuffer转字符串 * @param {ArrayBuffer} e 需要转换的ArrayBuffer类型数值 * @param {function} t 转换成功后的回调 */ getUint8Value(e, t) { for (var a = e, i = new DataView(a), n = "", s = 0; s < i.byteLength; s++) n += String.fromCharCode(i.getUint8(s)); t(n); }效果图显示: 微信小程序蓝牙通讯wx.onBLECharacteristicValueChange返回的value值是 ArrayBuffer类型,需要转换前端才能进行naf

April 26, 2019 · 1 min · jiezi

leetcode443. String Compression

题目要求Given an array of characters, compress it in-place.The length after compression must always be smaller than or equal to the original array.Every element of the array should be a character (not int) of length 1.After you are done modifying the input array in-place, return the new length of the array. Follow up:Could you solve it using only O(1) extra space? Example 1:Input:[“a”,“a”,“b”,“b”,“c”,“c”,“c”]Output:Return 6, and the first 6 characters of the input array should be: [“a”,“2”,“b”,“2”,“c”,“3”]Explanation:“aa” is replaced by “a2”. “bb” is replaced by “b2”. “ccc” is replaced by “c3”. Example 2:Input:[“a”]Output:Return 1, and the first 1 characters of the input array should be: [“a”]Explanation:Nothing is replaced. Example 3:Input:[“a”,“b”,“b”,“b”,“b”,“b”,“b”,“b”,“b”,“b”,“b”,“b”,“b”]Output:Return 4, and the first 4 characters of the input array should be: [“a”,“b”,“1”,“2”].Explanation:Since the character “a” does not repeat, it is not compressed. “bbbbbbbbbbbb” is replaced by “b12”.Notice each digit has it’s own entry in the array. Note:All characters have an ASCII value in [35, 126].1 <= len(chars) <= 1000.对字符串进行简单的压缩操作,压缩的规则是,如果出现多个重复的字母,则用字母加上字母出现的字数进行表示。如果字母只出现一次,则不记录次数。思路和代码核心思路是用三个指针分别记录三个下标:p1: 记录压缩后的内容的插入下标p2: 记录当前相同字符串的起始位置p3: 记录当前和起始位置比较的字符串的位置一旦出现p3的值不等于p2或是p3的值大于字符数组的长度,则将压缩结果从p1开始填写,实现O(1)的空间复杂度。 public int compress(char[] chars) { int p1 = 0; int p2 = 0; int p3 = 1; while(p2 < chars.length) { if(p3 >= chars.length || chars[p3] != chars[p2]) { int length = p3 - p2; chars[p1++] = chars[p2]; if(length != 1) { int count = 0; while(length != 0) { int num = length % 10; for(int i = p1+count ; i>p1 ; i–) { chars[i] = chars[i-1]; } chars[p1] = (char)(‘0’ + num); length /= 10; count++; } p1 += count; } p2 = p3; } p3++; } return p1; } ...

April 16, 2019 · 2 min · jiezi

干货|Spring Cloud Bus 消息总线介绍

继上一篇 干货|Spring Cloud Stream 体系及原理介绍 之后,本期我们来了解下 Spring Cloud 体系中的另外一个组件 Spring Cloud Bus (建议先熟悉 Spring Cloud Stream,不然无法理解 Spring Cloud Bus 内部的代码)。Spring Cloud Bus 对自己的定位是 Spring Cloud 体系内的消息总线,使用 message broker 来连接分布式系统的所有节点。Bus 官方的 Reference 文档 比较简单,简单到连一张图都没有。这是最新版的 Spring Cloud Bus 代码结构(代码量比较少):Bus 实例演示在分析 Bus 的实现之前,我们先来看两个使用 Spring Cloud Bus 的简单例子。所有节点的配置新增Bus 的例子比较简单,因为 Bus 的 AutoConfiguration 层都有了默认的配置,只需要引入消息中间件对应的 Spring Cloud Stream 以及 Spring Cloud Bus 依赖即可,之后所有启动的应用都会使用同一个 Topic 进行消息的接收和发送。Bus 对应的 Demo 已经放到了 github 上: https://github.com/fangjian0423/rocketmq-binder-demo/tree/master/rocketmq-bus-demo 。 该 Demo 会模拟启动 5 个节点,只需要对其中任意的一个实例新增配置项,所有节点都会新增该配置项。访问任意节点提供的 Controller 提供的获取配置的地址(key为hangzhou):curl -X GET ‘http://localhost:10001/bus/env?key=hangzhou’所有节点返回的结果都是 unknown,因为所有节点的配置中没有 hangzhou 这个 key。Bus 内部提供了 EnvironmentBusEndpoint 这个 Endpoint 通过 message broker 用来新增/更新配置。访问任意节点该 Endpoint 对应的 url /actuator/bus-env?name=hangzhou&value=alibaba 进行配置项的新增(比如访问 node1 的url):curl -X POST ‘http://localhost:10001/actuator/bus-env?name=hangzhou&value=alibaba’ -H ‘content-type: application/json’然后再次访问所有节点 /bus/env 获取配置:$ curl -X GET ‘http://localhost:10001/bus/env?key=hangzhou’unknown%~ ⌚$ curl -X GET ‘http://localhost:10002/bus/env?key=hangzhou’unknown%~ ⌚$ curl -X GET ‘http://localhost:10003/bus/env?key=hangzhou’unknown%~ ⌚$ curl -X GET ‘http://localhost:10004/bus/env?key=hangzhou’unknown%~ ⌚$ curl -X GET ‘http://localhost:10005/bus/env?key=hangzhou’unknown%~ ⌚$ curl -X POST ‘http://localhost:10001/actuator/bus-env?name=hangzhou&value=alibaba’ -H ‘content-type: application/json’~ ⌚$ curl -X GET ‘http://localhost:10005/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10004/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10003/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10002/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10001/bus/env?key=hangzhou’alibaba%可以看到,所有节点都新增了一个 key 为 hangzhou 的配置,且对应的 value 是 alibaba。这个配置项是通过 Bus 提供的 EnvironmentBusEndpoint 完成的。这里引用 程序猿DD 画的一张图片,Spring Cloud Config 配合 Bus 完成所有节点配置的刷新来描述之前的实例(本文实例不是刷新,而是新增配置,但是流程是一样的):部分节点的配置修改比如在 node1 上指定 destination 为 rocketmq-bus-node2 (node2 配置了 spring.cloud.bus.id 为 rocketmq-bus-node2:10002,可以匹配上) 进行配置的修改:curl -X POST ‘http://localhost:10001/actuator/bus-env/rocketmq-bus-node2?name=hangzhou&value=xihu’ -H ‘content-type: application/json’访问 /bus/env 获取配置(由于在 node1 上发送消息,Bus 也会对发送方的节点 node1 进行配置修改):~ ⌚$ curl -X POST ‘http://localhost:10001/actuator/bus-env/rocketmq-bus-node2?name=hangzhou&value=xihu’ -H ‘content-type: application/json’~ ⌚$ curl -X GET ‘http://localhost:10005/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10004/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10003/bus/env?key=hangzhou’alibaba%~ ⌚$ curl -X GET ‘http://localhost:10002/bus/env?key=hangzhou’xihu%~ ⌚$ curl -X GET ‘http://localhost:10001/bus/env?key=hangzhou’xihu%可以看到,只有 node1 和 node2 修改了配置,其余的 3 个节点配置未改变。Bus 的实现Bus 概念介绍事件Bus 中定义了远程事件 RemoteApplicationEvent,该事件继承了 Spring 的事件 ApplicationEvent,而且它目前有 4 个具体的实现:EnvironmentChangeRemoteApplicationEvent: 远程环境变更事件。主要用于接收一个 Map<String, String> 类型的数据并更新到 Spring 上下文中 Environment 中的事件。文中的实例就是使用这个事件并配合 EnvironmentBusEndpoint 和 EnvironmentChangeListener 完成的。AckRemoteApplicationEvent: 远程确认事件。Bus 内部成功接收到远程事件后会发送回 AckRemoteApplicationEvent 确认事件进行确认。RefreshRemoteApplicationEvent: 远程配置刷新事件。配合 @RefreshScope 以及所有的 @ConfigurationProperties 注解修饰的配置类的动态刷新。UnknownRemoteApplicationEvent:远程未知事件。Bus 内部消息体进行转换远程事件的时候如果发生异常会统一包装成该事件。Bus 内部还存在一个非 RemoteApplicationEvent 事件 - SentApplicationEvent 消息发送事件,配合 Trace 进行远程消息发送的记录。这些事件会配合 ApplicationListener 进行操作,比如 EnvironmentChangeRemoteApplicationEvent 配了 EnvironmentChangeListener 进行配置的新增/修改:public class EnvironmentChangeListener implements ApplicationListener<EnvironmentChangeRemoteApplicationEvent> { private static Log log = LogFactory.getLog(EnvironmentChangeListener.class); @Autowired private EnvironmentManager env; @Override public void onApplicationEvent(EnvironmentChangeRemoteApplicationEvent event) { Map<String, String> values = event.getValues(); log.info(“Received remote environment change request. Keys/values to update " + values); for (Map.Entry<String, String> entry : values.entrySet()) { env.setProperty(entry.getKey(), entry.getValue()); } }}收到其它节点发送来的 EnvironmentChangeRemoteApplicationEvent 事件之后调用 EnvironmentManager#setProperty 进行配置的设置,该方法内部针对每一个配置项都会发送一个 EnvironmentChangeEvent 事件,然后被 ConfigurationPropertiesRebinder 所监听,进行 rebind 操作新增/更新配置。Actuator EndpointBus 内部暴露了 2 个 Endpoint,分别是 EnvironmentBusEndpoint 和 RefreshBusEndpoint,进行配置的新增/修改以及全局配置刷新。它们对应的 Endpoint id 即 url 是 bus-env 和 bus-refresh。配置Bus 对于消息的发送必定涉及到 Topic、Group 之类的信息,这些内容都被封装到了 BusProperties 中,其默认的配置前缀为 spring.cloud.bus,比如:spring.cloud.bus.refresh.enabled 用于开启/关闭全局刷新的 Listener。spring.cloud.bus.env.enabled 用于开启/关闭配置新增/修改的 Endpoint。spring.cloud.bus.ack.enabled 用于开启开启/关闭 AckRemoteApplicationEvent 事件的发送。spring.cloud.bus.trace.enabled 用于开启/关闭消息记录 Trace 的 Listener。消息发送涉及到的 Topic 默认用的是 springCloudBus,可以配置进行修改,Group 可以设置成广播模式或使用 UUID 配合 offset 为 lastest 的模式。每个 Bus 应用都有一个对应的 Bus id,官方取值方式较复杂:${vcap.application.name:${spring.application.name:application}}:${vcap.application.instance_index:${spring.application.index:${local.server.port:${server.port:0}}}}:${vcap.application.instance_id:${random.value}}建议手动配置 Bus id,因为 Bus 远程事件中的 destination 会根据 Bus id 进行匹配:spring.cloud.bus.id=${spring.application.name}-${server.port}Bus 底层分析Bus 的底层分析无非牵扯到这几个方面:消息是如何发送的;消息是如何接收的;destination 是如何匹配的;远程事件收到后如何触发下一个 action;BusAutoConfiguration 自动化配置类被 @EnableBinding(SpringCloudBusClient.class) 所修饰。@EnableBinding 的用法在上期文章 干货|Spring Cloud Stream 体系及原理介绍 中已经说明,且它的 value 为 SpringCloudBusClient.class,会在 SpringCloudBusClient 中基于代理创建出 input 和 output 的 DirectChannel:public interface SpringCloudBusClient { String INPUT = “springCloudBusInput”; String OUTPUT = “springCloudBusOutput”; @Output(SpringCloudBusClient.OUTPUT) MessageChannel springCloudBusOutput(); @Input(SpringCloudBusClient.INPUT) SubscribableChannel springCloudBusInput();}springCloudBusInput 和 springCloudBusOutput 这两个 Binding 的属性可以通过配置文件进行修改(比如修改 topic):spring.cloud.stream.bindings: springCloudBusInput: destination: my-bus-topic springCloudBusOutput: destination: my-bus-topic消息的接收的发送:// BusAutoConfiguration@EventListener(classes = RemoteApplicationEvent.class) // 1public void acceptLocal(RemoteApplicationEvent event) { if (this.serviceMatcher.isFromSelf(event) && !(event instanceof AckRemoteApplicationEvent)) { // 2 this.cloudBusOutboundChannel.send(MessageBuilder.withPayload(event).build()); // 3 }}@StreamListener(SpringCloudBusClient.INPUT) // 4public void acceptRemote(RemoteApplicationEvent event) { if (event instanceof AckRemoteApplicationEvent) { if (this.bus.getTrace().isEnabled() && !this.serviceMatcher.isFromSelf(event) && this.applicationEventPublisher != null) { // 5 this.applicationEventPublisher.publishEvent(event); } // If it’s an ACK we are finished processing at this point return; } if (this.serviceMatcher.isForSelf(event) && this.applicationEventPublisher != null) { // 6 if (!this.serviceMatcher.isFromSelf(event)) { // 7 this.applicationEventPublisher.publishEvent(event); } if (this.bus.getAck().isEnabled()) { // 8 AckRemoteApplicationEvent ack = new AckRemoteApplicationEvent(this, this.serviceMatcher.getServiceId(), this.bus.getAck().getDestinationService(), event.getDestinationService(), event.getId(), event.getClass()); this.cloudBusOutboundChannel .send(MessageBuilder.withPayload(ack).build()); this.applicationEventPublisher.publishEvent(ack); } } if (this.bus.getTrace().isEnabled() && this.applicationEventPublisher != null) { // 9 // We are set to register sent events so publish it for local consumption, // irrespective of the origin this.applicationEventPublisher.publishEvent(new SentApplicationEvent(this, event.getOriginService(), event.getDestinationService(), event.getId(), event.getClass())); }}利用 Spring 事件的监听机制监听本地所有的 RemoteApplicationEvent 远程事件(比如 bus-env 会在本地发送 EnvironmentChangeRemoteApplicationEvent 事件,bus-refresh 会在本地发送 RefreshRemoteApplicationEvent 事件,这些事件在这里都会被监听到)。判断本地接收到的事件不是 AckRemoteApplicationEvent 远程确认事件(不然会死循环,一直接收消息,发送消息…)以及该事件是应用自身发送出去的(事件发送方是应用自身),如果都满足执行步骤 3。构造 Message 并将该远程事件作为 payload,然后使用 Spring Cloud Stream 构造的 Binding name 为 springCloudBusOutput 的 MessageChannel 将消息发送到 broker。@StreamListener 注解消费 Spring Cloud Stream 构造的 Binding name 为 springCloudBusInput 的 MessageChannel,接收的消息为远程消息。如果该远程事件是 AckRemoteApplicationEvent 远程确认事件并且应用开启了消息追踪 trace 开关,同时该远程事件不是应用自身发送的(事件发送方不是应用自身,表示事件是其它应用发送过来的),那么本地发送 AckRemoteApplicationEvent 远程确认事件表示应用确认收到了其它应用发送过来的远程事件,流程结束。如果该远程事件是其它应用发送给应用自身的(事件的接收方是应用自身),那么进行步骤 7 和 8,否则执行步骤 9。该远程事件不是应用自身发送(事件发送方不是应用自身)的话,将该事件以本地的方式发送出去。应用自身一开始已经在本地被对应的消息接收方处理了,无需再次发送。如果开启了 AckRemoteApplicationEvent 远程确认事件的开关,构造 AckRemoteApplicationEvent 事件并在远程和本地都发送该事件(本地发送是因为步骤 5 没有进行本地 AckRemoteApplicationEvent 事件的发送,也就是自身应用对自身应用确认; 远程发送是为了告诉其它应用,自身应用收到了消息)。如果开启了消息记录 Trace 的开关,本地构造并发送 SentApplicationEvent 事件bus-env 触发后所有节点的 EnvironmentChangeListener 监听到了配置的变化,控制台都会打印出以下信息:o.s.c.b.event.EnvironmentChangeListener : Received remote environment change request. Keys/values to update {hangzhou=alibaba}如果在本地监听远程确认事件 AckRemoteApplicationEvent,都会收到所有节点的信息,比如 node5 节点的控制台监听到的 AckRemoteApplicationEvent 事件如下:ServiceId [rocketmq-bus-node5:10005] listeners on {“type”:“AckRemoteApplicationEvent”,“timestamp”:1554124670484,“originService”:“rocketmq-bus-node5:10005”,“destinationService”:”",“id”:“375f0426-c24e-4904-bce1-5e09371fc9bc”,“ackId”:“750d033f-356a-4aad-8cf0-3481ace8698c”,“ackDestinationService”:"",“event”:“org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent”}ServiceId [rocketmq-bus-node5:10005] listeners on {“type”:“AckRemoteApplicationEvent”,“timestamp”:1554124670184,“originService”:“rocketmq-bus-node1:10001”,“destinationService”:"",“id”:“91f06cf1-4bd9-4dd8-9526-9299a35bb7cc”,“ackId”:“750d033f-356a-4aad-8cf0-3481ace8698c”,“ackDestinationService”:"",“event”:“org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent”}ServiceId [rocketmq-bus-node5:10005] listeners on {“type”:“AckRemoteApplicationEvent”,“timestamp”:1554124670402,“originService”:“rocketmq-bus-node2:10002”,“destinationService”:"",“id”:“7df3963c-7c3e-4549-9a22-a23fa90a6b85”,“ackId”:“750d033f-356a-4aad-8cf0-3481ace8698c”,“ackDestinationService”:"",“event”:“org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent”}ServiceId [rocketmq-bus-node5:10005] listeners on {“type”:“AckRemoteApplicationEvent”,“timestamp”:1554124670406,“originService”:“rocketmq-bus-node3:10003”,“destinationService”:"",“id”:“728b45ee-5e26-46c2-af1a-e8d1571e5d3a”,“ackId”:“750d033f-356a-4aad-8cf0-3481ace8698c”,“ackDestinationService”:"",“event”:“org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent”}ServiceId [rocketmq-bus-node5:10005] listeners on {“type”:“AckRemoteApplicationEvent”,“timestamp”:1554124670427,“originService”:“rocketmq-bus-node4:10004”,“destinationService”:"",“id”:“1812fd6d-6f98-4e5b-a38a-4b11aee08aeb”,“ackId”:“750d033f-356a-4aad-8cf0-3481ace8698c”,“ackDestinationService”:"",“event”:“org.springframework.cloud.bus.event.EnvironmentChangeRemoteApplicationEvent”}那么回到本章节开头提到的 4 个问题,我们分别做一下解答:消息是如何发送的: 在 BusAutoConfiguration#acceptLocal 方法中通过 Spring Cloud Stream 发送事件到 springCloudBus topic 中。消息是如何接收的: 在 BusAutoConfiguration#acceptRemote 方法中通过 Spring Cloud Stream 接收 springCloudBus topic 的消息。destination 是如何匹配的: 在 BusAutoConfiguration#acceptRemote 方法中接收远程事件方法里对 destination 进行匹配。远程事件收到后如何触发下一个 action: Bus 内部通过 Spring 的事件机制接收本地的 RemoteApplicationEvent 具体的实现事件再做下一步的动作(比如 EnvironmentChangeListener 接收了 EnvironmentChangeRemoteApplicationEvent 事件, RefreshListener 接收了 RefreshRemoteApplicationEvent 事件)。总结Spring Cloud Bus 自身内容还是比较少的,不过还是需要提前了解 Spring Cloud Stream 体系以及 Spring 自身的事件机制,在此基础上,才能更好地理解 Spring Cloud Bus 对本地事件和远程事件的处理逻辑。目前 Bus 内置的远程事件较少,大多数为配置相关的事件,我们可以继承 RemoteApplicationEvent 并配合 @RemoteApplicationEventScan 注解构建自身的微服务消息体系。本文作者:中间件小哥阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

April 15, 2019 · 4 min · jiezi

面试别再问我String了

阅读原文:面试别再问我String了字符串广泛应用 在Java 编程中,在 Java 中字符串属于对象,Java 提供了 String 类来创建和操作字符串。String 简介String定义:public final class String implements java.io.Serializable, Comparable<String>, CharSequence {}为什么设计为不可变类呢?String设计为不可变类主要考虑到:效率和安全。效率:1.在早期的JVM实现版本中,被final修饰的方法会被转为内嵌调用以提升执行效率。而从Java SE5/6开始,就渐渐摈弃这种方式了。因此在现在的Java SE版本中,不需要考虑用final去提升方法调用效率。只有在确定不想让该方法被覆盖时,才将方法设置为final。2.缓存hashcode,String不可变,所以hashcode不变,这样缓存才有意义,不必重新计算。安全:String常被作为网络连接,文件操作等参数类型,倘若可改变,会出现意想不到的结果。测试掌握程度为了不浪费你的时间,请看下面的题目,若你一目了然,可以跳过本文了。public class Test { public static void main(String[] args) { String str1 = “HelloFlyapi”; String str2 = “HelloFlyapi”; String str3 = new String(“HelloFlyapi”); String str4 = “Hello”; String str5 = “Flyapi”; String str6 = “Hello” + “Flyapi”; String str7 = str4 + str5; System.out.println(“str1 == str2 result: " + (str1 == str2)); System.out.println(“str1 == str3 result: " + (str1 == str3)); System.out.println(“str1 == str6 result: " + (str1 == str6)); System.out.println(“str1 == str7 result: " + (str1 == str7)); System.out.println(“str1 == str7.intern() result: " + (str1 == str7.intern())); System.out.println(“str3 == str3.intern() result: " + (str3 == str3.intern())); }}String 的创建方式从上面的题中你会知道,String的创建方式有两种:直接赋值此方式在方法区中字符串常量池中创建对象String str = “flyapi”;构造器此方式在堆内存创建对象String str = new String();分析要理解String,那么要了解JVM内存中的栈(stack)、堆(heap)和方法区。简要图如下:str1 == str2String str1 = “HelloFlyapi”;String str2 = “HelloFlyapi”;System.out.println(str1 == str2); // true当执行第一句时,JVM会先去常量池中查找是否存在HelloFlyapi,当存在时直接返回常量池里的引用;当不存在时,会在字符创常量池中创建一个对象并返回引用。当执行第二句时,同样的道理,由于第一句已经在常量池中创建了,所以直接返回上句创建的对象的引用。str1 == str3String str1 = “HelloFlyapi”;String str3 = new String(“HelloFlyapi”);System.out.println(str1 == str3); // false执行第一句,同上第一句。执行第二句时,会在堆(heap)中创建一个对象,当字符创常量池中没有‘HelloFlyapi’时,会在常量池中也创建一个对象;当常量池中已经存在了,就不会创建新的了。str1 == str6String str1 = “HelloFlyapi”;String str6 = “Hello” + “Flyapi”;System.out.println(str1 == str6); // true由于"Hello"和"Flyapi"都是常量,编译时,第二句会被自动编译为‘String str6 = “HelloFlyapi”;’str1 == str7String str1 = “HelloFlyapi”;String str4 = “Hello”;String str5 = “Flyapi”;String str7 = str4 + str5;System.out.println(str1 == str7); // false其中前三句变量存储的是常量池中的引用地址。第四句执行时,JVM会在堆(heap)中创建一个以str4为基础的一个StringBuilder对象,然后调用StringBuilder的append()方法完成与str5的合并,之后会调用toString()方法在堆(heap)中创建一个String对象,并把这个String对象的引用赋给str7。常用方法下面是 String 类支持的方法,更多详细,参看 Java String API 文档:方法描述char charAt(int index)返回指定索引处的 char 值。int compareTo(Object o)把这个字符串和另一个对象比较。int compareTo(String anotherString)按字典顺序比较两个字符串。boolean endsWith(String suffix)测试此字符串是否以指定的后缀结束。boolean equals(Object anObject)将此字符串与指定的对象比较。boolean equalsIgnoreCase(String anotherString)将此 String 与另一个 String 比较,不考虑大小写。byte[] getBytes()使用平台的默认字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。byte[] getBytes(String charsetName)使用指定的字符集将此 String 编码为 byte 序列,并将结果存储到一个新的 byte 数组中。int indexOf(int ch)返回指定字符在此字符串中第一次出现处的索引。int indexOf(int ch, int fromIndex)返回在此字符串中第一次出现指定字符处的索引,从指定的索引开始搜索。int indexOf(String str)返回指定子字符串在此字符串中第一次出现处的索引。int indexOf(String str, int fromIndex)返回指定子字符串在此字符串中第一次出现处的索引,从指定的索引开始。String intern()返回字符串对象的规范化表示形式。int lastIndexOf(int ch)返回指定字符在此字符串中最后一次出现处的索引。int lastIndexOf(int ch, int fromIndex)返回指定字符在此字符串中最后一次出现处的索引,从指定的索引处开始进行反向搜索。int lastIndexOf(String str)返回指定子字符串在此字符串中最右边出现处的索引。int lastIndexOf(String str, int fromIndex)返回指定子字符串在此字符串中最后一次出现处的索引,从指定的索引开始反向搜索。int length()返回此字符串的长度。boolean matches(String regex)告知此字符串是否匹配给定的正则表达式。String replace(char oldChar, char newChar)返回一个新的字符串,它是通过用 newChar 替换此字符串中出现的所有 oldChar 得到的。String replaceAll(String regex, String replacement)使用给定的 replacement 替换此字符串所有匹配给定的正则表达式的子字符串。String replaceFirst(String regex, String replacement)使用给定的 replacement 替换此字符串匹配给定的正则表达式的第一个子字符串。String[] split(String regex)根据给定正则表达式的匹配拆分此字符串。String[] split(String regex, int limit)根据匹配给定的正则表达式来拆分此字符串。boolean startsWith(String prefix)测试此字符串是否以指定的前缀开始。boolean startsWith(String prefix, int toffset)测试此字符串从指定索引开始的子字符串是否以指定前缀开始。String substring(int beginIndex)返回一个新的字符串,它是此字符串的一个子字符串。String substring(int beginIndex, int endIndex)返回一个新字符串,它是此字符串的一个子字符串。char[] toCharArray()将此字符串转换为一个新的字符数组。String toLowerCase()使用默认语言环境的规则将此 String 中的所有字符都转换为小写。String toUpperCase()使用默认语言环境的规则将此 String 中的所有字符都转换为大写。String trim()返回字符串的副本,忽略前导空白和尾部空白。String相关由于String的不可变性导致,字符串变更时效率低下,在之后得JDK版本中出现了StringBuilder和StringBuffer.类可变性线程安全String不可变安全StringBuffer可变安全StringBuilder可变非安全使用选择当有少量连接操作时,使用String当单线程下有大量连接操作时,使用StringBuilder当多线程下有大量连接操作时,使用StringBuffer常见String面试题String str = new String(“abc”)创建了多少个实例?这个问题其实是不严谨的,但面试一般会遇到,所以我们要补充来说明。类的加载和执行要分开来讲:创建了两个当加载类时,“abc"被创建并驻留在了字符创常量池中(如果先前加载中没有创建驻留过)。当执行此句时,因为"abc"对应的String实例已经存在于字符串常量池中,所以JVM会将此实例复制到会在堆(heap)中并返回引用地址。通过字节码我们可以看到:源码:String str = new String(“abc”)字节码: Code: 0: new #2 // class java/lang/String 3: dup 4: ldc #3 // String abc 6: invokespecial #4 // Method java/lang/String."<init>”:(Ljava/lang/String;) 9: astore_1 10: return执行时仅(#2)创建了一个对象。关于这个面试题,可以看看一个超大牛的回答:http://rednaxelafx.iteye.com/…本文优先发布于微信公众号:码上实战 和 GitHub 上。GitHub flyhero ...

March 28, 2019 · 2 min · jiezi

C++ string的SSO

C++的string相对于C语言的string完善了很多,通过运算符重载可以很直观的进行字符串的拼接等操作。GCC 5.0以后的版本采用了__SSO__(短字符串优化)的策略替换了原本的__COW__优化,我写了几段代码来验证了一下新的实现的一些细节。PS:这里的所有的内容只是特定平台特定编译器的特定行为。平台: Windows 64位 MinGW 7.3.0#include <iostream>using namespace std;int main(int argc, char* argv[]){ string a = “123456789abcde”; //15个char string s(“this is the edge”); //16个char string longstr(“Scaramouche, scaramouche will you do the Fandango”); //长串 在heap分配 cout << &a << " " << (void*)a.c_str() << endl; cout << &s << " " << (void*)s.c_str() << endl; cout << &longstr << " " << (void*)longstr.c_str() << endl; cout << sizeof(char*) << endl; cout << sizeof(s) << endl; return 0;}0x62fde0 0x62fdf00x62fdc0 0x7d17d00x62fda0 0x7d1b40832在这里我通过c_str打印串开始的地址。从输出我们可以看出来string占32个字节,其中16个字节实际上用于存储字符。我们创建出来的三个对象地址相差0x20(32)个字节,后两个的指针地址于对象的地址相差很远,应该是在堆上动态分配的内存,而第一个字符串的存储地址(0x62fdf0)于对象的起始地址(0x62fde0)只差__0x10__,而对象大小__0x20__,所有这个串实际上就是存储在这个程序栈中.上文的 SSO 实际上值得就是短的字符串(strlen(s)<15,即最多包含15个char和'0’)直接存储在对象里,更长的串再存储在堆上开辟的空间里。C++的string与xstring和sds在很多情况下很相像。 ...

March 8, 2019 · 1 min · jiezi

Table Store: 海量结构化数据实时备份实战

数据备份简介在信息技术与数据管理领域,备份是指将文件系统或数据库系统中的数据加以复制,一旦发生灾难或者错误操作时,得以方便而及时地恢复系统的有效数据和正常运作。在实际备份过程中,最好将重要数据制作三个或三个以上的备份,并且放置在不同的场所异地备援,以供日后回存之用。备份有两个不同的目的,其主要的目的是在数据丢失后恢复数据,无论数据是被删除还是被损坏。备份的第二个目的是根据用户定义的数据保留策略从较早的时间恢复数据,通常在备份应用程序中配置需要备份多长时间的数据副本。由于备份系统至少会包含一个被认为值得保存的所有数据的副本,因此对数据存储的要求可能会很高,组织此存储空间和管理备份过程可能是一项复杂的任务。如今,有许多不同类型的数据存储设备可用于进行备份,还可以通过许多不同的方式来安排这些设备以提供地理冗余,数据安全性和可移植性。在将数据发送到其存储位置之前,会选择,提取和操作它们,目前已经有许多不同的技术来优化备份过程,其中包括处理打开的文件(open files)和实时数据源的优化,以及压缩,加密和重复数据删除等。每个备份方案都应包括演习过程,以验证正在备份的数据的可靠性,更重要的是要认识到任何备份方案中涉及的限制和人为因素。Table Store备份需求分析对于存储系统而言,数据的安全可靠永远是第一位的,要保障数据尽可能高的可靠性,需要从两个方面保障:存储系统本身的数据可靠性:表格存储(Table Store)是阿里云自研的面向海量结构化数据存储的Serverless NoSQL多模型数据库,提供了99.9999999%的数据可靠性保证,在业界属于非常非常高的标准了。误操作后能恢复数据:误操作永远都无法避免,要做的是当误操作发生的时候能尽快恢复,那么就需要有备份数据存在。对于备份,有两种方案,一个是部署同城或异地灾备,这种代价高费用高,更多的用于社会基础信息或金融信息。另一种是将数据备份到另一个价格更低廉的系统,当万一出现误操作的时候,可以有办法恢复就行。一般可以选择文件存储系统,比如阿里云OSS。Table Store备份恢复方案简介下图为Table Store备份恢复的逻辑结构图,基于全增量一体的通道服务我们可以很容易的构建一整套的数据备份和数据恢复方案,同时具备了实时增量备份能力和秒级别的恢复能力。只要提前配置好备份和恢复的计划,整个备份恢复系统可以做到完全的自动化进行。Table Store备份恢复方案实战目前表格存储虽然未推出官方的备份和恢复功能,但是笔者会step-by-step的带大家基于表格存储的通道服务设计属于自己的专属备份恢复方案,实战步骤会分为备份和恢复两部分。1. 备份预准备阶段:需要确定待备份的数据源和备份的目的源,在此次的实战中,分别对应着TableStore的表和OSS的Bucket。确定备份计划和策略使用通道服务SDK编写代码执行备份策略并监测备份过程2. 恢复执行文件恢复确定备份计划和策略备份策略需要确定备份内容、备份时间和备份方式,目前常见的备份策略主要有以下几类。全量备份(Full Backup): 把硬盘或数据库内的所有文件、文件夹或数据进行一次性的复制。增量备份(Incremental Backup): 对上一次的全量备份或增量备份后更新的数据进行的备份。差异备份(Differential Backup): 提供运行全量备份后变更文件的备份。选择式备份:对系统数据的一部分进行的备份。冷备份:系统处于停机或维护状态下的备份。这种情况下,备份的数据与系统该时段的数据应该完全一致。热备份:系统处于正常运转状态下的备份。这种情况下,由于系统中的数据可能随时在更新,备份的数据相对于系统的真实数据可能会有一定的滞后。在此次的实战中,笔者对于备份计划和策略的选择如下(这里可以根据自己的需求进行自定义设计,然后利用通道服务的SDK来完成相应的需求)。备份内容:TableStore 数据表备份时间:全量备份定期执行(时间可调,默认为一周)。增量备份根据配置定期执行,表格存储的通道服务可以保障数据的严格有序性,每个增量文件是流式append的,可随时被消费。备份方式:全量备份+增量备份,热备份。使用通道服务SDK编写代码这部分会结合代码片段的形式讲解,详细的代码后续会开源出去(链接会更新在本文中),尽请期待。在进行实战之前,推荐先阅读通道服务Java SDK的 使用文档。创建一个全量+增量类型的Tunnel,这里可以使用SDK或者官网控制台进行创建。 private static void createTunnel(TunnelClient client, String tunnelName) { CreateTunnelRequest request = new CreateTunnelRequest(TableName, tunnelName, TunnelType.BaseAndStream); CreateTunnelResponse resp = client.createTunnel(request); System.out.println(“RequestId: " + resp.getRequestId()); System.out.println(“TunnelId: " + resp.getTunnelId());}了解通道服务自动化数据处理框架 在通道服务的快速开始里,我们可以看到用户只需要传入处理数据的process函数和shutdown函数即可完成自动化的数据处理。在process函数的入参中会带有List,而StreamRecord包装的正是TableStore里每一行的数据,包括Record的类型,主键列,属性列,用户写入Record的时间戳等。设计TableStore每一行数据的持久化格式 本次实战中我们使用CSV文件格式,当然你也可以用pb或者其它格式,CSV的优点是可读性比较强,每一行数据会对应CSV文件的一行,持久化的格式如下图所示,CSV文件会有四列, TimeStamp列是数据写入TableStore的时间戳(全量时都为0,增量备份为具体的时间戳),RecordType是数据行的操作类型(PUT, UPDATE和DELETE),PrimaryKey为主键列的JSON字符串表示,RecordColumns为属性列的JSON字符串表示。转换部分的核心代码参见如下代码片段,笔者处理这部分用的是univocity-parsers(CSV)和Gson库,有几个地方需要特别注意下:1). Gson反序列化Long类型会转为Number类型,可能会造成进度丢失,有若干解决办法,笔者采用的是将其转为String类型序列化。2). 对于binary类型的数据的特殊处理,笔者进行了base64的编解码。3). 可以直接流式写入OSS中,减少本地持久化的消耗。this.gson = new GsonBuilder().registerTypeHierarchyAdapter(byte[].class, new ByteArrayToBase64TypeAdapter()) .setLongSerializationPolicy(LongSerializationPolicy.STRING).create();// ByteArrayOutputStream到ByteArrayInputStream会有一次array.copy, 可考虑用管道或者NIO channel.public void streamRecordsToOSS(List<StreamRecord> records, String bucketName, String filename, boolean isNewFile) { if (records.size() == 0) { LOG.info(“No stream records, skip it!”); return; } try { CsvWriterSettings settings = new CsvWriterSettings(); ByteArrayOutputStream out = new ByteArrayOutputStream(); CsvWriter writer = new CsvWriter(out, settings); if (isNewFile) { LOG.info(“Write csv header, filename {}”, filename); List<String> headers = Arrays.asList(RECORD_TIMESTAMP, RECORD_TYPE, PRIMARY_KEY, RECORD_COLUMNS); writer.writeHeaders(headers); System.out.println(writer.getRecordCount()); } List<String[]> totalRows = new ArrayList<String[]>(); LOG.info(“Write stream records, num: {}”, records.size()); for (StreamRecord record : records) { String timestamp = String.valueOf(record.getSequenceInfo().getTimestamp()); String recordType = record.getRecordType().name(); String primaryKey = gson.toJson( TunnelPrimaryKeyColumn.genColumns(record.getPrimaryKey().getPrimaryKeyColumns())); String columns = gson.toJson(TunnelRecordColumn.genColumns(record.getColumns())); totalRows.add(new String[] {timestamp, recordType, primaryKey, columns}); } writer.writeStringRowsAndClose(totalRows); // write to oss file ossClient.putObject(bucketName, filename, new ByteArrayInputStream(out.toByteArray())); } catch (Exception e) { e.printStackTrace(); }}执行备份策略并监测备份过程运行通道服务的自动化数据框架,挂载上一步中设计的备份策略代码。public class TunnelBackup { private final ConfigHelper config; private final SyncClient syncClient; private final CsvHelper csvHelper; private final OSSClient ossClient; public TunnelBackup(ConfigHelper config) { this.config = config; syncClient = new SyncClient(config.getEndpoint(), config.getAccessId(), config.getAccessKey(), config.getInstanceName()); ossClient = new OSSClient(config.getOssEndpoint(), config.getAccessId(), config.getAccessKey()); csvHelper = new CsvHelper(syncClient, ossClient); } public void working() { TunnelClient client = new TunnelClient(config.getEndpoint(), config.getAccessId(), config.getAccessKey(), config.getInstanceName()); OtsReaderConfig readerConfig = new OtsReaderConfig(); TunnelWorkerConfig workerConfig = new TunnelWorkerConfig( new OtsReaderProcessor(csvHelper, config.getOssBucket(), readerConfig)); TunnelWorker worker = new TunnelWorker(config.getTunnelId(), client, workerConfig); try { worker.connectAndWorking(); } catch (Exception e) { e.printStackTrace(); worker.shutdown(); client.shutdown(); } } public static void main(String[] args) { TunnelBackup tunnelBackup = new TunnelBackup(new ConfigHelper()); tunnelBackup.working(); }}监测备份过程备份过程的监控可以通过表格存储控制台或者 DescribeTunnel 接口完成,DescribeTunnel接口可以实时获取当前Tunnel下每一个Channel的Client(可以自定义), 消费总行数和消费位点等信息。比如用户有下午2点-3点的数据需要同步,若DescribeTunnel获取到的消费位点为下午2点半,则表示备份到一半了,若获取到的消费位点为下午3点,则代表2点-3点的数据已经备份完毕了。执行文件恢复在数据备份完成后,我们可以根据需求进行全量的恢复和部分数据的恢复,来降低RTO。在恢复数据时,可以有一些措施来优化恢复的性能:从OSS下载时,可以使用流式下载或者分片下载的方式,必要时可以增加并发。写入TableStore时,可以使用并发BatchWrite的方式。下图为Restore的实例代码(略去若干细节),首先根据需求算出需要下载的文件名(和备份策略对应),然后用OSS的流式下载读取相应的文件,最后用并发的BatchWrite方式写入TableStore的恢复表中。public class TunnelRestore { private ConfigHelper config; private final SyncClient syncClient; private final CsvHelper csvHelper; private final OSSClient ossClient; public TunnelRestore(ConfigHelper config) { this.config = config; syncClient = new SyncClient(config.getEndpoint(), config.getAccessId(), config.getAccessKey(), config.getInstanceName()); ossClient = new OSSClient(config.getOssEndpoint(), config.getAccessId(), config.getAccessKey()); csvHelper = new CsvHelper(syncClient, ossClient); } public void restore(String filename, String tableName) { csvHelper.parseStreamRecordsFromCSV(filename, tableName); } public static void main(String[] args) { TunnelRestore restore = new TunnelRestore(new ConfigHelper()); restore.restore(“FullData-1551767131130.csv”, “testRestore”); }}总结本文首先介绍了Table Store备份的需求,接着介绍了使用表格存储的通道服务进行数据备份和恢复的原理,最后结合实际的代码片段讲述了如何一步步的构建Tabel Store数据备份恢复方案。参考链接4 Steps to Create Your Backup Planhttps://en.wikipedia.org/wiki/Backup本文作者:琸然阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 7, 2019 · 2 min · jiezi

利用blink+MQ实现流计算中的超时统计问题

案例与解决方案汇总页:阿里云实时计算产品案例&解决方案汇总一. 背景介绍菜鸟的物流数据本身就有链路复杂、实操节点多、汇总维度多、考核逻辑复杂的特点,对于实时数据的计算存在很大挑战。经过仓配ETL团队的努力,目前仓配实时数据已覆盖了绝大多数场景,但是有这样一类特殊指标:“晚点超时指标”(例如:出库超6小时未揽收的订单量),仍存在实时汇总计算困难。原因在于:流计算是基于消息触发计算的,若没有消息到达到则无法计算,这类指标恰好是要求在指定的超时时间计算出有多少未达到的消息。然而,这类指标对于指导实操有着重要意义,可以告知运营小二当前多少订单积压在哪些作业节点,应该督促哪些实操人员加快作业,这对于物流的时效KPI达成至关重要。之前的方案是:由产品前端根据用户的请求查询OLAP数据库,由OLAP从明细表出结果。大促期间,用户请求量大,加之数据量大,故对OLAP的明细查询造成了比较大的压力。二. 解决方案2.1 问题定义“超时晚点指标” 是指,一笔订单的两个相邻的实操节点node_n-1 、node_n 的完成时间 time_n-1、time_n,当满足 : time_n is null && current_time - time_n-1 > kpi_length 时,time_flag_n 为 true , 该笔订单计入 超时晚点指标的计数。如下图,有一笔订单其 node_1 为出库节点,时间为time_1 = ‘2018-06-18 00:00:00’ ,运营对出库与揽收之间考核的时长 kpi_length = 6h, 那么当前自然时间 current_time > ‘2018-06-18 06:00:00’ 时,且node_2揽收节点的time_2 为null,则该笔订单的 timeout_flag_2 = true , “出库超6小时未揽收订单量” 加1。由于要求time_2 为null,即要求没有揽收消息下发的情况下让流计算做汇总值更新,这违背了流计算基于消息触发的基本原理,故流计算无法直接算出这种“超时晚点指标”。决问题的基本思路是:在考核时刻(即 kpi_time = time_n-1+kpi_length )“制造”出一条消息下发给流计算,触发汇总计算。继续上面的例子:在考核时刻“2018-06-18 06:00:00”利用MetaQ定时消息功能“制造”出一条消息下发给流计算汇总任务,触发对该笔订单的 time_out_flag_2 的判断,增加汇总计数。同时,还利用 Blink 的Retraction 机制,当time_2 由null变成有值的时候,Blink 可以对 time_out_flag_2 更新,重新计数。2.2 方案架构如上图所示:Step1: Blink job1 接收来自上游系统的订单数据,做清洗加工,生成订单明细表:dwd_ord_ri,利用TT下发给Blink job2 和 Blink job3。Step2:Blink job2 收到 dwd_ord_ri后,对每笔订单算出考核时刻 kpi_time = time_n-1+kpi_length,作为MetaQ消息的“TIMER_DELIVER_MS” 属性,写入MetaQ。MetaQ的定时消息功能,可以根据用户写入的TIMER_DELIVER_MS 在指定时刻下发给消费者,即上图中的Blink job3。Step3:Blink job3 接收 TT、MetaQ 两个消息源,先做Join,再对time_flag判断,最后做Aggregate计算。同一笔订单,dwd_ord_ri、timing_msg任意一个消息到来,都会触发join,time_flag判断,aggregate重新计算一遍,Blink的Retraction可对结果进行实时更新。2.3 实现细节本方案根据物流场景中多种实操节点、多种考核时长的特点,从Blink SQL代码 和 自定义Sink两方面做了特殊设计,从而实现了灵活配置、高效开发。(1) Blink job2 — 生成定时消息关键Blink SQL 代码如下。约定每条record的第一个字段为投递时间列表,即MetaQ向消费者下发消息的时刻List,也就是上面所说的多个考核时刻。第二个字段为保序字段,比如在物流场景中经常以订单code、运单号作为保序主键。该代码实现了对每个出库的物流订单,根据其出库时间,向后延迟6小时(21600000毫秒)、12小时(43200000毫秒)、24小时(86400000毫秒)由MetaQ向消费者下发三个定时消息。create table metaq_timing_msg(deliver_time_list varchar comment ‘投递时间列表’, – 约定第一个字段为投递时间listlg_code varchar comment ‘物流订单code’, – 约定第二字段为保序主键node_name varchar comment ‘节点名称’,node_time varchar comment ‘节点时间’,)WITH(type = ‘custom’,class = ‘com.alibaba.xxx.xxx.udf.MetaQTimingMsgSink’,tag = ‘store’,topic = ‘blink_metaq_delay_msg_test’,producergroup = ‘blinktest’,retrytimes = ‘5’,sleeptime = ‘1000’);insert into metaq_timing_msgselectconcat_ws(’,’,cast( (UNIX_TIMESTAMP(store_out_time)*1000 + 21600000) as varchar), –6小时cast( (UNIX_TIMESTAMP(store_out_time)*1000 + 43200000) as varchar), –12小时cast( (UNIX_TIMESTAMP(store_out_time)*1000 + 86400000) as varchar) –24小时) as deliver_time_list,lg_code,‘wms’ as node_name,store_out_time as node_timefrom(selectlg_code,FIRST_VALUE(store_out_time) as store_out_timefrom srctablegroup by lg_code)bwhere store_out_time is not null ;(2) Blink 自定义Sink — MetaQTimingMsg SinkBlink的当前版本还不支持 MetaQ的定时消息功能的Sink,故利用 Blink的自定义Sink功能,并结合菜鸟物流数据的特点开发了MetaQTimingMsg Sink。关键代码如下(实现 writeAddRecord 方法)。@Overridepublic void writeAddRecord(Row row) throws IOException {Object deliverTime = row.getField(0);String[] deliverTimeList = deliverTime.toString().split(",");for(String dTime:deliverTimeList){ String orderCode = row.getField(1).toString(); String key = orderCode + “_” + dTime; Message message = newMessage(row, dTime, key); boolean result = sendMessage(message,orderCode); if(!result){ LOG.error(orderCode + " : " + dTime + " send failed"); } }}private Message newMessage(Row row,String deliverMillisec,String key){ //Support Varbinary Type Insert Into MetaQ Message message = new Message(); message.setKeys(key); message.putUserProperty(“TIMER_DELIVER_MS”,deliverMillisec); int arity = row.getArity(); Object[] values = new Object[arity]; for(int i=0;i<arity;i++){ values[i]=row.getField(i); } String lineStr=org.apache.commons.lang3.StringUtils.join(values, FIELD_DELIMITER); try { byte[] bytes = lineStr.getBytes(ENCODING); message.setBody(bytes); message.setWaitStoreMsgOK(true); } catch (UnsupportedEncodingException e) { LOG.error(“create new message error”,e); } return message;}private boolean sendMessage(Message message,String orderCode){ long retryTime = 0; boolean isSendSuccess = true; if(message != null){ message.setTopic(topicName); message.setTags(tagName); } SendResult result = producer.send(message, new MessageQueueSelector() { @Override public MessageQueue select(List<MessageQueue> list, Message message, Object o) { …. // 针对物流订单code的hash算法 return list.get(index.intValue()); } },orderCode); if(!result.getSendStatus().equals(SendStatus.SEND_OK)){ LOG.error("" + orderCode +" write to metaq result is " + result.getSendStatus().toString()); isSendSuccess = false; } return isSendSuccess;}}(3)Blink job3 — 汇总计算关键Blink SQL 代码如下,统计了每个仓库的“出库超6小时未揽收物理订单”、“出库超12小时未揽收物理订单”、“出库超24小时未揽收物理订单”的汇总值。代码中使用了“stringLast()”函数处理来自dwd_ord_ri的每条消息,以取得每个物流订单的最新出库揽收情况,利用Blink Retraction机制,更新汇总值。create view dws_store_view as select t1.store_code, max(t1.store_name) as store_name, count(case when length(trim(t1.store_out_time)) = 19 and t1.tms_collect_time is null and NOW()-UNIX_TIMESTAMP(t1.store_out_time,‘yyyy-MM-dd HH:mm:ss’) >= 21600 then t2.lg_code end ) as tms_not_collect_6h_ord_cnt, —出库超6小时未揽收物流订单量 count(case when length(trim(t1.store_out_time)) = 19 and t1.tms_collect_time is null and NOW()-UNIX_TIMESTAMP(t1.store_out_time,‘yyyy-MM-dd HH:mm:ss’) >= 43200 then t2.lg_code end ) as tms_not_collect_12h_ord_cnt,—出库超6小时未揽收物流订单量 count(case when length(trim(t1.store_out_time)) = 19 and t1.tms_collect_time is null and NOW()-UNIX_TIMESTAMP(t1.store_out_time,‘yyyy-MM-dd HH:mm:ss’) >= 86400 then t2.lg_code end ) as tms_not_collect_24h_ord_cnt —出库超6小时未揽收物流订单量from ( select lg_code, coalesce(store_code,’-1’) as store_code, store_name, store_out_time, tms_collect_time from ( select lg_code, max(store_code) as store_code, max(store_name) as store_name, stringLast(store_out_time) as store_out_time, stringLast(tms_collect_time)as tms_collect_time, from dwd_ord_ri group by lg_code ) a ) t1left outer join ( select lg_code, from timing_msg where node_name = ‘wms’ group by lg_code) t2on t1.lg_code = t2.lg_codegroup by t1.store_code ;三. 方案优势3.1 配置灵活我们从“Blink SQL 代码” 和“自定义MetaQ” 两个方面设计,用户可以根据具体的业务场景,在Blink SQL的一个view里就能实现多种节点多种考核时间的定时消息生成,而不是针对每一个实操节点的每一种定时指标都要写一个view,这样大大节省了代码量,提升了开发效率。例如对于仓库节点的出库超6小时未揽收、超12小时未揽收、超24小时未揽收,这三个指标利用上述方案,仅需在Blink job2的中metaq_timing_msg的第一个字段deliver_time_list中拼接三个kpi_length,即6小时、12小时、24小时为一个字符串即可,由MetaQTimingMsg Sink自动拆分成三条消息下发给MetaQ。对于不同的节点的考核,仅需在node_name,node_time填写不同的节点名称和节点实操时间即可。3.2 主键保序如2.3节所述,自定义的Sink中 实现了MetaQ的 MessageQueueSelector 接口的 select() 方法,同时在Blink SQL 生成的MetaQ消息默认第二个字段为保序主键字段。从而,可以根据用户自定义的主键,保证同一主键的所有消息放在同一个通道内处理,从而保证按主键保序,这对于流计算非常关键,能够实现数据的实时准确性。3.3 性能优良让专业的团队做专业的事。个人认为,这种大规模的消息存储、消息下发的任务本就应该交给“消息中间件”来处理,这样既可以做到计算与消息存储分离,也可以方便消息的管理,比如针对不同的实操节点,我们还可以定义不同的MetaQ的tag。另外,正如2.2节所述,我们对定时消息量做了优化。考虑到一笔订单的属性字段或其他节点更新会下发多条消息,我们利用了Blink的FIRST_VALUE函数,在Blink job2中同一笔订单的的一种考核指标只下发一条定时消息,大大减少了消息量,减轻了Blink的写压力,和MetaQ的存储。四. 自我介绍马汶园 阿里巴巴 -菜鸟网络—数据部 数据工程师菜鸟仓配实时研发核心成员,主导多次仓配大促实时数据研发,对利用Blink的原理与特性解决物流场景问题有深入思考与理解。本文作者:付空阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

March 6, 2019 · 3 min · jiezi

【现代C++】性能控的工具箱之string_view

本篇文章从string_view引入的背景出发,依次介绍了其相关的知识点及使用方式,然后对常见的使用陷阱进行了说明,最后对该类型做总结。一、背景在日常C/C++编程中,我们常进行数据的传递操作,比如,将数据传给函数。当数据占用的内存较大时,减少数据的拷贝可以有效提高程序的性能。在C中指针是完成这一目的的标准数据结构,而C++引入了安全性更高的引用类型。所以在C++中若传递的数据仅仅只读,const string&成了C++的天然的方式。但这并非完美,从实践来看,它至少有以下几方面问题:字符串字面值、字符数组、字符串指针的传递仍要数据拷贝这三类低级数据类型与string类型不同,传入时,编译器需要做隐式转换,即需要拷贝这些数据生成string临时对象。const string&指向的实际上是这个临时对象。通常字符串字面值较小,性能损耗可以忽略不计;但字符串指针和字符数组某些情况下可能会比较大(比如读取文件的内容),此时会引起频繁的内存分配和数据拷贝,会严重影响程序的性能。substr O(n)复杂度这是一个特别常用的函数,好在std::string提供了这个函数,美中不足的是其每次都返回一个新生成的子串,很容易引起性能热点。实际上我们本意并不是要改变原字符串,为什么不在原字符串基础上返回呢?在C++17中引入了string_view,能很好的解决以上两个问题。二、std::string_view从名字出发,我们可以类比数据库视图,view表示该类型不会为数据分配存储空间,而且该数据类型只能用来读。该数据类型可通过{数据的起始指针,数据的长度}两个元素表示,实际上该数据类型的实例不会具体存储原数据,仅仅存储指向的数据的起始指针和长度,所以这个开销是非常小的。要使用字符串视图,需要引入<string_view>,下面介绍该数据类型主要的API。这些API基本上都有constexpr修饰,所以能在编译时很好地处理字符串字面值,从而提高程序效率。2.1 构造函数constexpr string_view() noexcept;constexpr string_view(const string_view& other) noexcept = default;constexpr string_view(const CharT* s, size_type count);constexpr string_view(const CharT* s);基本上都是自解释的,唯一需要说明的是:为什么我们代码string_view foo(string(“abc”))可以编译通过,但为什么没有对应的构造函数?实际上这是因为string类重载了string到string_view的转换操作符:operator std::basic_string_view<CharT, Traits>() const noexcept;所以,string_view foo(string(“abc”))实际执行了两步操作:string(“abc”)转换为string_view对象astring_view使用对象本篇文章从string_view引入的背景,2.2 自定义字面量自定义字面量也是C++17新增的特性,提高了常量的易读。下面的代码取值cppreference,能很好地说明自定义字面值和字符串语义的差异。#include <string_view>#include <iostream> int main(){ using namespace std::literals; std::string_view s1 = “abc\0\0def”; std::string_view s2 = “abc\0\0def"sv; std::cout << “s1: " << s1.size() << " "” << s1 << “"\n”; std::cout << “s2: " << s2.size() << " "” << s2 << “"\n”;}输出:s1: 3 “abc"s2: 8 “abc^@^@def"以上例子能很好看清二者的语义区别,\0对于字符串而言,有其特殊的意义,即表示字符串的结束,字符串视图根本不care,它关心实际的字符个数。2.3 成员函数下面列举其成员函数:忽略了函数的返回值,若函数有重载,括号内用…填充。这样可以对其有个整体轮廓。// 迭代器begin()end()cbegin()cend()rbegin()rend()crbegin()crend() // 容量size()length()max_size()empty() // 元素访问operator[](size_type pos)at(size_type pos)front()back()data() // 修改器remove_prefix(size_type n)remove_suffix(size_type n)swap(basic_string_view& s) copy(charT* s, size_type n, size_type pos = 0)string_view substr(size_type pos = 0, size_type n = npos)compare(…)starts_with(…)ends_with(…)find(…)rfind(…)find_first_of(…)find_last_of(…) find_first_not_of(…)find_last_not_of(…)从函数列表来看,几乎跟string的只读函数一致,使用string_view的方式跟string基本一致。有几个地方需要特别说明:string_view的substr函数的时间复杂度是O(1),解决了背景部分的第二个问题。修改器中的三个函数仅会修改string_view的数据指向,不会修改指向的数据。除此之外,函数名基本是自解释的。2.4 示例Haskell中有一个常用函数lines,会将字符串切割成行存储在容器里。下面我们用C++来实现string-版本#include <string>#include <iostream>#include <vector>#include <algorithm>#include <sstream>void lines(std::vector<std::string> &lines, const std::string &str) { auto sep{"\n”}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string::npos) end = str.length(); lines.push_back(str.substr(start, end - start)); start = str.find_first_not_of(sep, end + 1); }}上面我们用const std::string &类型接收待分割的字符串,若我们传入指向较大内存的字符指针时,会影响程序效率。使用std::string_view可以避免这种情况:string_view-版本#include <string>#include <iostream>#include <vector>#include <algorithm>#include <sstream>#include <string_view>void lines(std::vector<std::string> &lines, std::string_view str) { auto sep{"\n”}; size_t start{str.find_first_not_of(sep)}; size_t end{}; while (start != std::string_view::npos) { end = str.find_first_of(sep, start + 1); if (end == std::string_view::npos) end = str.length(); lines.push_back(std::string{str.substr(start, end - start)}); start = str.find_first_not_of(sep, end + 1); }}上面的例子仅仅是把string类型修改成了string_view就获得了性能上的提升。一般情况下,将程序中的string换成string_view的过程是比较直观的,这得益于两者的成员函数的相似性。但并不是所有的“翻译”过程都是这样的,比如:void lines(std::vector<std::string> &lines, const std::string& str) { std::stringstream ss(str); std::string line; while (std::getline(ss, line, ‘\n’)) { lines.push_back(line); }}这个版本使用stringstream实现lines函数。由于stringstream没有相应的构造函数接收string_view类型参数,所以没法采用直接替换的方式,所以翻译过程要复杂点。三、使用陷阱世上没有免费的午餐。不恰当的使用string_view也会带来一系列的问题。string_view范围内的字符可能不包含\0如#include <iostream>#include <string_view>int main() { std::string_view str{“abc”, 1}; std::cout << str.data() << std::endl; return 0;}本来是要打印a,但输出了abc。这是因为字符串相关的函数都有一条兼容C的约定:\0代表字符串的结尾。上面的程序打印从开始到字符串结束的所有字符,虽然str包含的有效字符是a,但cout认\0。好在这块内存空间有合法的字符串结尾符,如果str指向的是一个没有\0的字符数组,程序很有可能会出现内存问题,所以我们在将string_view类型的数据传入接收字符串的函数时要非常小心。2.从[const] char构造string_view对象时间复杂度O(n) 这是因为获取字符串的长度需要从头开始遍历。如果对[const] char类型仅仅是一些O(1)的操作,相比直接使用[const] char*,转为string_view是没有性能优势的。只不过是相比const string&,string_view少了拷贝的损耗。实际上我们完全可以用[const] char*接收所有的字符串,但这个类型太底层了,不便使用。在某些情况下,我们转为string_view可能仅仅是想用其中的一些函数,比如substr。3.string_view指向的内容的生命周期可能比其本身短string_view并不拥有其指向内容的所有权,用Rust的术语来说,它仅仅是暂时borrow(借用)了它。如果拥有者提前释放了,你还在使用这些内容,那会出现内存问题,这跟悬挂指针(dangling pointer)或悬挂引用(dangling references)很像。Rust专门有套机制在编译时分析变量的生命期,保证borrow的资源在使用期间不会被释放,但C++没有这样的检查,需要人工保证。下面列出一些典型的问题情况:std::string_view sv = std::string{“hello world”}; string_view foo() { std::string s{“hello world”}; return string_view{s};}auto id(std::string_view sv) { return sv; }int main() { std::string s = “hello”; auto sv = id(s + " world”); }四、总结string_view解决了一些痛点,但同时也引入了指针和引用的一些老问题。C++标准并没有对这个类型做太多的约束,这引来的问题是我们可以像平常的变量一样以多种方式使用它,如,可以传参,可以作为函数返回值,可以做普遍变量,甚至我们可以放到容器里。随着使用场景的复杂,人工是很难保证指向的内容的生命周期足够长。所以,推荐的使用方式:仅仅作为函数参数,因为如果该参数仅仅在函数体内使用而不传递出去,这样使用是安全的。 请关注我的公众号哦。 ...

March 5, 2019 · 2 min · jiezi

java:String

String类是final类,它内部的方法也默认被final修饰,不能重写.字符串常量池当这样声明一个字符串 String str = “hello java”;JVM会检测字符串常量池中是否存在这个值的字符串,如果存在,就直接赋值给str,否则创建一个新的,再赋值给str.当连续用同样的方式声明两个字符串并作比较 String str1 = “hellojava”;String str2 = “hellojava”;boolean flag = str1==str2;//true结果为true.当我们这样声明 String str1 = “hello”;String str2 = “java”;String str3 = “hellojava”;String str4 = str1+str2;boolean flag = str3==str4;//false结果为false.这是因为str4是有两个引用类型结合而成,它的值在编译期无法确定.如果将str1和str2声明为final类型final String str1 = “hello”;final String str2 = “java”;String str3 = “hellojava”;String str4 = str1+str2;boolean flag = str3==str4;//truestr1和str2都是被final修饰的字符串常量,那么str4在编译期就可以被确定.因此结果是true.通过new创建字符串String str3 = “hellojava”;String str4 = new String(“hellojava”);boolean flag = str3==str4;//false这是因为new会在堆中创建一个hellojava的实例对象,并用栈中的str4指向它.而str3指向的是方法区中字符串常量池中的hellojava.当然堆中的hellojava指向的也是字符串常量池中的hellojava(如果存在的话).要了解一下str3和str4的声明做了什么?先声明str3.会在方法区的字符串常量池中直接创建一个字符串"hellojava",使str3指向它.紧接着用new创建str4时,会先查找字符串常量池中是否包含字符串"hellojava".如果有直接返回,没有就在常量池中创建.然后在堆中创建实例对象,并将这个实例对象指向常量池中的字符串"hellojava",然后将实例对象赋给栈中的str4.

March 4, 2019 · 1 min · jiezi

C语言下实现的String库

C语言的StringC语言作为一门古老的高级语言,对于字符串的支持十分的薄弱。入门时我们就知道我们使用数组来包含一串的ASCII字符来作为字符串的实现,如char arr[] = “hello world!";这样基于长度固定的数组的实现方式就导致了C的字符串的长度是不可变的,但是arr[]的内容却是可变的。这样的设计导致很多时候我们对字符串的处理十分的麻烦与危险,像我之前写的哈夫曼编码解码的时候,为了盛放解码后的结果,我不得不创建一个非常大的静态数组或者动态分配内存来放置函数产生的长度不定的字符串。相较于其后辈(如Python/Java,C++基本兼容C的语法,尽管C++实现了自己的string类),C在很多方面也是比较异类的,比如C使用’\0’来标志字符串的结束,因而len(arr)这样的操作的复杂度就达到了O(n),这是一个比较大的开销,而Pascal/Python等的实现都可以做到O(1),同时,由于char类型本身就是最短的整型再加上C语言的弱类型的类型系统,‘a’- 32也是完全有效的语法,而在Python中这会引发TypeError. 这些问题在C语言诞生的年代不是大问题,毕竟当时没有那么多字符串的处理需求,而且C主要的应用场景也比较偏底层。而现在,一些选择C实现的程序需要频繁的处理字符串(如 Redis,需要频繁的处理键值对),为了应对这种场景,很多很有意思的自己的String实现都被提了出来。在这里我主要是介绍ccan的xstring和sds的一些实现的思路。xstring/** * struct xstring - string metadata * @str: pointer to buf * @len: current length of buf contents * @cap: maximum capacity of buf * @truncated: -1 indicates truncation */typedef struct xstring { char *str; size_t len; size_t cap; int truncated;} xstring;xstring xstrNew(const size_t size){ char str; xstring x; if (size < 1) { errno = EINVAL; return NULL; } str = malloc(size);//mark 1 if (!str) return NULL; x = malloc(sizeof(struct xstring));//mark 2 if (!x) { free(str); return NULL; } xstrInit(x, str, size, 0); return x;}透过xstring结构体与xstrNew(const size_t size)这个创建新的xstring的函数,ccan的这个实现的思路就比较清晰了,xstring结构体本身占据内存,但是并不存储字符串,字符串在mark 1被分配存储空间,而结构体在mark 2被分配内存。PS:在刚刚学习使用C来实现数据结构的时候,我很疑惑为何不能直接struct xstring newStruct(){ struct xstring s; return &s;}直到后来才逐渐明白了栈上的变量与动态分配的变量的微妙的区别,s在这个函数返回后就已经被销毁了,传出的这个地址是无效的,而对他的引用很可能会导致段错误(segment fault),操作系统,编译原理等课真的会让自己对于程序设计语言获得更深的理解。而且这种写法当时很有吸引力,毕竟不用malloc,不用强制类型转换。这种野指针是很多很难修正的错误的来源,有兴趣的同学可以去学习一下Rust语言的所有权系统,很多的概念很有意思。| xstring | -> | str |可以看出xstring的实现中内存是分为两个部分的。Note: xstring只需要编译器支持C89/90。sdsredis sds(simple dynamic string)是Redis对于str的实现,在这里有官方对于sds实现的一些技巧的介绍,在这里我会将SDS实现的主要的细节介绍以下。// sds 类型typedef char sds;// sdshdr 结构struct sdshdr { // buf 已占用长度 int len; // buf 剩余可用长度 int free; // 实际保存字符串数据的地方 // 利用c99(C99 specification 6.7.2.1.16)中引入的 flexible array member,通过buf来引用sdshdr后面的地址, // 详情google “flexible array member” char buf[];};和上面的实现不太一样的是sds只存储存储的字符串长度以及剩余长度,但是最引人瞩目的无疑是最后的那一个数组声明:char buf[];结构体中竟然没有声明数组的大小,这样好像与我们对于数组一贯的印象不符,但是这是合法的特性,叫做柔性数组。具体的语法细节我不再介绍,但是注意以下几点sizeof(struct sdshdr) == sizeof(len) + sizeof(buf),在x86_64上典型值应该为8个字节(4 + 4),这说明buf[]没有实际占据空间,一个64位系统下的指针就要8个字节。上面的写法是C99 only的,这个特性应该来自于以下这种写法,struct header { size_t len; unsigned char data[1];};这种写法下data就是一个 unsigned char型的指针,可以通过它用来访问存储的字符串。//在分配内存的时候,结构体中存储了一个字符,其他的(n-1)个空间在//紧随结构体结束地址的地方// | struct (char) | (n - 1) char |ptr = malloc(sizeof(struct header) + (n-1));对比sds中的实现,sds中不存储任何一个数据,只有一个不占据内存空间的标记代表,所有的数据都存储在结构体所占空间后面| struct | str |我们来看这有什么用: / * 返回 sds buf 的已占用长度 / static inline size_t sdslen(const sds s) { struct sdshdr sh = (void)(s-(sizeof(struct sdshdr))); return sh->len; } / * 返回 sds buf 的可用长度 / static inline size_t sdsavail(const sds s) { struct sdshdr sh = (void)(s-(sizeof(struct sdshdr))); return sh->free; } / * 创建一个指定长度的 sds * 如果给定了初始化值 init 的话,那么将 init 复制到 sds 的 buf 当中 * * T = O(N) */ sds sdsnewlen(const void init, size_t initlen) { struct sdshdr sh; // 有 init ? // O(N) if (init) { sh = zmalloc(sizeof(struct sdshdr)+initlen+1); } else { sh = zcalloc(sizeof(struct sdshdr)+initlen+1); } // 内存不足,分配失败 if (sh == NULL) return NULL; sh->len = initlen; sh->free = 0; // 如果给定了 init 且 initlen 不为 0 的话 // 那么将 init 的内容复制至 sds buf // O(N) if (initlen && init) memcpy(sh->buf, init, initlen); // 加上终结符 sh->buf[initlen] = ‘\0’; // 返回 buf 而不是整个 sdshdr return (char)sh->buf; } 我们创建一个新的sds的时候,分配sizeof(struct sdshdr) + len + 1大小的空间,len代表不包含结束符号在内的容量,最后我们返回的是字符串开始的地址,这个返回的地址可以直接作为一般的字符串被其他库函数等使用,即Redis所说的二进制兼容的(因为其内部也使用'0’结尾)。同时结构体的地址可以通过用字符串的地址减去结构体的大小得到struct sdshdr sh = (void)(s-(sizeof(struct sdshdr)));这样一来sds可以在常数时间内获得字符串的长度。#include <stdio.h>#include “./src/simple_dynamic_str.h"int main() { sds s = sdsnew(“Hello World! K&R”); printf("%s\n”, s); printf("%zu %zu\n”, sdslen(s), sdsavail(s)); printf("%c",s[0]); return 0;}结果:Hello World! K&R16 0H这种通过指针的计算获得结构体的地址的方式还是比较少见的技巧,我也只是在Linux内核的task_struct结构体中见识过类似的技巧,当然那个更复杂。这种操作是很危险的,但是C数组在这方面其实也没有好多少(并没有多出数组越界检查等),不是吗?在字符串较短时,结构体占据放入空间是比较可观的,更新版本的Redis优化了不同长度的字符串结构体的定义。/ Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. /struct attribute ((packed)) sdshdr5 { unsigned char flags; / 3 lsb of type, and 5 msb of string length / char buf[];};struct attribute ((packed)) sdshdr8 { uint8_t len; / used / uint8_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[];};struct attribute ((packed)) sdshdr16 { uint16_t len; / used / uint16_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[];};struct attribute ((packed)) sdshdr32 { uint32_t len; / used / uint32_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits / char buf[];};struct attribute ((packed)) sdshdr64 { uint64_t len; / used / uint64_t alloc; / excluding the header and null terminator / unsigned char flags; / 3 lsb of type, 5 unused bits */ char buf[];};总结这篇文章中有些技巧还是有些难度的,像sds我也是花了一些时间才弄明白其原理,这里的两种实现我个人更偏爱第二种,但是这毕竟是二等公民,没有语言级别的支持是硬伤。所以如果真的需要大量处理字符串,特别是非纯ASCII码,左转Java/Python etc.reference:redis sds(simple dynamic string)ccan xstringRedis设计与实现https://stackoverflow.com/que…https://redis.io/topics/inter… ...

March 1, 2019 · 3 min · jiezi

【Go】string 也是引用类型

原文链接:https://blog.thinkeridea.com/…初学 Go 语言的朋友总会在传 []byte 和 string 之间有着很多纠结,实际上是没有了解 string 与 slice 的本质,而且读了一些程序源码,也发现很多与之相关的问题,下面类似的代码估计很多初学者都写过,也充分说明了作者当时内心的纠结:package mainimport “bytes"func xx(s []byte) []byte{ …. return s}func main(){ s := “xxx” s = string(xx([]byte(s))) s = string(bytes.Replace([]byte(s), []byte(“x”), []byte(”"), -1))}虽然这样的代码并不是来自真实的项目,但是确实有人这样设计,单从设计上看就很糟糕了,这样设计的原因很多人说:“slice 是引用类型,传递引用类型效率高呀”,主要原因不了解两者的本质,加上文档上说 Go 的引用类型有两种:slice 和 map ,这个在面试中也是经常遇到的吧。上面这个例子如果觉得有点基础和可爱,下面这个例子貌似并不那么容易说明其存在的问题了吧。package mainfunc xx(s *string) *string{ …. return s}func main(){ s := “xx” s = *xx(&s) ss :=[]*string{} ss = append(ss, &s)}指针效率高,我就用指针多好,可以减少内存分配呀,设计函数都接收指针变量,程序性能会有很大提升,在实际的项目中这种例子也不少见,我想通过这篇文档来帮助初学者走出误区,减少适得其反的优化技巧。slice 的定义在之前 “【Go】深入剖析slice和array” 一文中说了 slice 在内存中的存储模式,slice 本身包含一个指向底层数组的指针,一个 int 类型的长度和一个 int 类型的容量, 这就是 slice 的本质, []byte 本身也是一个 slice,只是底层数组存储的元素是 byte。下面这个图就是 slice 的在内存中的状态:看一下 reflect.SliceHeader 如何定义 slice 在内存中的结构吧:type SliceHeader struct { Data uintptr Len int Cap int}slice 是引用类型是 slice 本身会包含一个地址,在传递 slice 时只需要分配 SliceHeader 就好了, 而 SliceHeader 只包含了三个 int 类型,相当于传递一个 slice 就只需要拷贝 SliceHeader,而不用拷贝整个底层数组,所以才说 slice 是引用类型的。那么字符串呢,计算机中我们处理的大多数问题都和字符串有关,难道传递字符串真的需要那么高的成本,需要借助 slice 和指针来减少内存开销吗。string 的定义reflect 包里面也定义了一个 StringHeader 看一下吧:type StringHeader struct { Data uintptr Len int}字符串只包含了两个 int 类型的数据,其中一个是指针,一个是字符串的长度,从定义上来看 string 也是引用类型。借助 unsafe 来分析一下情况是不是这样吧:package mainimport ( “reflect” “unsafe” “github.com/davecgh/go-spew/spew”)func xx(s string) { sh := *(*reflect.StringHeader)(unsafe.Pointer(&s)) spew.Dump(sh)}func main() { s := “xx” sh := *(reflect.StringHeader)(unsafe.Pointer(&s)) spew.Dump(sh) xx(s) xx(s[:1]) xx(s[1:])}上面这段代码的输出如下:(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee0, Len: (int) 1}(reflect.StringHeader) { Data: (uintptr) 0x10f5ee1, Len: (int) 1}可以发现前三个输出的指针都是同一个地址,第四个的地址发生了一个字节的偏移,分析来看传递字符串确实没有分配新的内存,同时和 slice 一样即使传递字符串的子串也不会分配新的内存空间,而是指向原字符串的中的一个位置。这样说来把 string 转成 []byte 还浪费的一个 int 的空间呢,需要分配更多的内存,真是适得其反呀,而且类型转换会发生内存拷贝,从 string 转为 []byte 才是真的把 string 底层数据全部拷贝一遍呢,真是得不偿失呀。string 的两个小特性字符串还有两个小特性,针对字面量(就是直接写在程序中的字符串),会创建在只读空间上,并且被复用,看一下下面的一个小例子:package mainimport ( “reflect” “unsafe” “github.com/davecgh/go-spew/spew”)func main() { a := “xx” b := “xx” c := “xxx” spew.Dump((reflect.StringHeader)(unsafe.Pointer(&a))) spew.Dump((reflect.StringHeader)(unsafe.Pointer(&b))) spew.Dump((*reflect.StringHeader)(unsafe.Pointer(&c)))}从输出可以了解到,相同的字面量会被复用,但是子串是不会复用空间的,这就是编译器给我们带来的福利了,可以减少字面量字符串占用的内存空间。(reflect.StringHeader) { Data: (uintptr) 0x10f5ea0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5ea0, Len: (int) 2}(reflect.StringHeader) { Data: (uintptr) 0x10f5f2e, Len: (int) 3}另一个小特性大家都知道,就是字符串是不能修改的,如果我们不希望调用函数修改我们的数据,最好传递字符串,高效有安全。不过有了 unsafe 这个黑魔法,字符串的这一个特性也就不那么可靠了。package mainimport ( “fmt” “reflect” “strings” “unsafe”)func main() { a := strings.Repeat(“x”, 10) fmt.Println(a) strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := reflect.SliceHeader{ Data: strHeader.Data, Len: strHeader.Len, Cap: strHeader.Len, } b := ([]byte)(unsafe.Pointer(&sliceHeader)) b[1] = ‘a’ fmt.Println(a)}从输出里面居然发现字符串被修改了, 我们没有办法直接修改字符串,但是可以利用 slice 和 string 本身结构的特性,创建一个 slice 让它的指针指向 string 的指针位置,然后借助 unsafe 把这个 SliceHeader 转成 []byte 来修改字符串,字符串确实被修改了。xxxxxxxxxxxaxxxxxxxx看了上面的例子是不是开始担心把字符串传给其它函数真的不会更改吗?感觉很不放心的样子,难道使用任何函数都要了解它的内部实现吗,其实这种情况极少发生,还记得之前说的那个字符串特性吗,字面量字符串会放到只读空间中,这个很重要,可以保证不是任何函数想修改我们的字符串就可以修改的。package mainimport ( “reflect” “unsafe”)func main() { defer func() { recover() }() a := “xx” strHeader := *(*reflect.StringHeader)(unsafe.Pointer(&a)) sliceHeader := reflect.SliceHeader{ Data: strHeader.Data, Len: strHeader.Len, Cap: strHeader.Len, } b := ([]byte)(unsafe.Pointer(&sliceHeader)) b[1] = ‘a’}运行上面的代码发生了一个运行时不可修复的错误,就是这个特性其它函数不能确保输入字符串是否是字面量,也是不会恶意修改我们字符串的了。unexpected fault address 0x1095dd5fatal error: fault[signal SIGBUS: bus error code=0x2 addr=0x1095dd5 pc=0x106c804]goroutine 1 [running]:runtime.throw(0x1095fde, 0x5) /usr/local/go/src/runtime/panic.go:608 +0x72 fp=0xc000040700 sp=0xc0000406d0 pc=0x10248d2runtime.sigpanic() /usr/local/go/src/runtime/signal_unix.go:387 +0x2d7 fp=0xc000040750 sp=0xc000040700 pc=0x1037677main.main() /Users/qiyin/project/go/src/github.com/yumimobi/test/a.go:22 +0x84 fp=0xc000040798 sp=0xc000040750 pc=0x106c804runtime.main() /usr/local/go/src/runtime/proc.go:201 +0x207 fp=0xc0000407e0 sp=0xc000040798 pc=0x1026247runtime.goexit() /usr/local/go/src/runtime/asm_amd64.s:1333 +0x1 fp=0xc0000407e8 sp=0xc0000407e0 pc=0x104da51关于字符串转 []byte 在 go-extend 扩展包中有直接的实现,这种用法在 go-extend 内部方法实现中也有大量使用, 实际上因为原数据类型和处理数据的函数类型不一致,使用这种方法转换字符串和 []byte 可以极大的提升程序性能exbytes.ToString 零成本的把 []byte 转为 string。exstrings.UnsafeToBytes 零成本的把 []byte 转为 string。上面这两个函数用的好,可以极大的提升我们程序的性能,关于 exstrings.UnsafeToBytes 我们转换不确定是否是字面量的字符串时就需要确保调用的函数不会修改我们的数据,这往常在调用 bytes 里面的方法十分有效。传字符串和字符串指针的区别之前分析了传递 slice 并没有 string 高效,何况转换数据类型本身就会发生数据拷贝。那么在这篇文章的第二个例子,为什么说传递字符串指针也不好呢,要了解指针在底层就是一个 int 类型的数据,而我们字符串只是两个 int 而已,另外如果了解 GC 的话,GC 只处理堆上的数据,传递指针字符串会导致数据逃逸到堆上,阅读标准库的代码会有很多注释说明避免逃逸到堆上,这样会极大的增加 GC 的开销,GC 的成本可谓是很高的呀。疑惑这篇文章说 “传递 slice 并没有 string 高效”,为什么还会有 bytes 包的存在呢,其中很多函数的功能和 strings 包的功能一致,只是把 string 换成了 []byte, 既然传递 []byte 没有 string 效率好,这个包存在的意义是什么呢。我们想一下转换数据类型是会发生数据拷贝,这个成本可是大的多呀,如果我们数据本身就是 []byte 类型,使用 strings 包就需要转换数据类型了。另外我们对比两个函数来看下一下即使传递 []byte 没有 string 效率好,但是标准库实现上却会导致两个函数有很大的性能差异的。strings.Repeat 函数:func Repeat(s string, count int) string { // Since we cannot return an error on overflow, // we should panic if the repeat will generate // an overflow. // See Issue golang.org/issue/16237 if count < 0 { panic(“strings: negative Repeat count”) } else if count > 0 && len(s)*count/count != len(s) { panic(“strings: Repeat count causes overflow”) } b := make([]byte, len(s)*count) bp := copy(b, s) for bp < len(b) { copy(b[bp:], b[:bp]) bp *= 2 } return string(b)}bytes.Repeat 函数:func Repeat(b []byte, count int) []byte { // Since we cannot return an error on overflow, // we should panic if the repeat will generate // an overflow. // See Issue golang.org/issue/16237. if count < 0 { panic(“bytes: negative Repeat count”) } else if count > 0 && len(b)*count/count != len(b) { panic(“bytes: Repeat count causes overflow”) } nb := make([]byte, len(b)*count) bp := copy(nb, b) for bp < len(nb) { copy(nb[bp:], nb[:bp]) bp *= 2 } return nb}上面两个函数的实现非常相似,除了类型不同 strings 包在处理完数据发生了一次类型转换,使用 bytes 只有一次内存分配,而 strings 是两次。我们可以借助 exbytes.ToString 函数把 bytes.Repeat 的返回没有任何成本的转换会我们需要的字符串,如果我们输入也是一个字符串的话,还可以借助 exstrings.UnsafeToBytes 来转换输入的数据类型。例如:s := exbytes.ToString(bytes.Repeat(exstrings.UnsafeToBytes(“x”), 10))不过这样写有点太麻烦了,实际上 exstrings 包里面正在修改 strings 里面一些类似函数的问题,所有的实现基本和标准库一致,只是把其中类型转换的部分用 exbytes.ToString 优化了一下,可以提升性能,也能提升开发效率。exstrings.UnsafeRepeat 函数:func UnsafeRepeat(s string, count int) string { // Since we cannot return an error on overflow, // we should panic if the repeat will generate // an overflow. // See Issue golang.org/issue/16237 if count < 0 { panic(“strings: negative Repeat count”) } else if count > 0 && len(s)*count/count != len(s) { panic(“strings: Repeat count causes overflow”) } b := make([]byte, len(s)*count) bp := copy(b, s) for bp < len(b) { copy(b[bp:], b[:bp]) bp *= 2 } return exbytes.ToString(b)}如果用上面的函数只需要下面这样写就可以了:s:=exstrings.UnsafeRepeat(“x”, 10)go-extend 里面还收录了很多实用的方法,大家也可以多关注。总结千万不要为了使用 []byte 来优化 string 传递,类型转换成本很高,且 slice 本身也比 string 更大一些。程序中是使用 string 还是 []byte 需要根据数据来源和处理数据的函数来决定,一定要减少类型转换。关于使用 strings 还是 bytes 包的问题,主要关注点是数据原始类型以及想获得的数据类型来选择。减少使用字符串指针来优化字符串,这会增加 GC 的开销,具体可以参考 大堆中避免大量的GC开销 一文。转载:本文作者: 戚银(thinkeridea)本文链接: https://blog.thinkeridea.com/201902/go/string_ye_shi_yin_yong_lei_xing.html版权声明: 本博客所有文章除特别声明外,均采用 CC BY 4.0 CN协议 许可协议。转载请注明出处! ...

February 24, 2019 · 4 min · jiezi

PAT A1052

这个需要注意的是相关的string转整数或者double的函数;详见这个链接blog#include <iostream>#include <string>using namespace std;bool isPrime(int n) { if (n == 0 || n == 1) return false; for (int i = 2; i * i <= n; i++) if (n % i == 0) return false; return true;}int main() { int l, k; string s; cin >> l >> k >> s; for (int i = 0; i <= l - k; i++) { string t = s.substr(i, k); int num = stoi(t); if (isPrime(num)) { cout << t; return 0; } } cout << “404\n”; return 0;} ...

February 20, 2019 · 1 min · jiezi

leetcode389.Find The Difference

题目要求Given two strings s and t which consist of only lowercase letters.String t is generated by random shuffling string s and then add one more letter at a random position.Find the letter that was added in t.Example:Input:s = “abcd"t = “abcde"Output:eExplanation:’e’ is the letter that was added.假设两个只包含小写字母的字符串s和t,其中t是s中字母的乱序,并在某个位置上添加了一个新的字母。问添加的这个新的字母是什么?思路一:字符数组我们可以利用一个整数数组来记录所有字符出现的次数,在s中出现一次相应计数加一,在t中出现一次则减一。最后只需要遍历整数数组检查是否有某个字符计数大于0。则该字符就是多余的字符。 public char findTheDifference(String s, String t) { int[] count = new int[26]; for(int i = 0 ; i<t.length() ; i++) { if(i != t.length()-1) { count[s.charAt(i)-‘a’]++; } count[t.charAt(i)-‘a’]–; } for(int i = 0 ; i<count.length ; i++) { if(count[i] != 0) { return (char)(‘a’ + i); } } return ‘a’; }思路二:求和我们知道,字符对应的ascii码是唯一的,那么既然两个字符串相比只有一个多余的字符,那么二者的ascii码和相减就可以找到唯一的字符的ascii码。 public char findTheDifference2(String s, String t){ int value = 0; for(int i = 0 ; i<s.length() ; i++) { value -= s.charAt(i); value += t.charAt(i); } char result = (char)(value + t.charAt(t.length()-1)); return result; } ...

February 12, 2019 · 1 min · jiezi

让前端面试不再难(二)

昨天聊了一个算法题,今天接着聊!多聊几个。1、拍平数组(多维数组变成一维数组) let arr = [1,[2,3,[4],[5,6,[7]]],8]//[1,2,3,4,5,6,7,8] //这个有很多方法,我们一一说来 //第一种遍历数组,遍历过程遇到数组递归。 function flatten(arr, newArr) { //省去全局变量,还避开了函数嵌套闭包的形成。 newArr = newArr || [] for (let i = 0; i < arr.length; i++) { //如果是arr[i]是数组那么递归执行,并把当前arr[i]和已有newArr传进去继续push。 //如果不是直接push到newArr typeof arr[i] === ‘object’ ? flatten(arr[i], newArr) : newArr.push(arr[i]) } return newArr } console.log(flatten(arr)) //第二种,逻辑一样只不过遍历换成了reduce,如果读的比较困难请移步:https://segmentfault.com/a/1190000017510301 了解reduce function flatten1(arr) { return arr.reduce((newArr, item) => { return typeof item === ‘object’ ? newArr.concat(flatten1(item, newArr)) : (newArr.push(item), newArr) }, []) } console.log(flatten1(arr)) //第三种比较简单 function flatten2(arr) { //join会默认过滤数组内部[],算是一个奇淫技巧。 return arr.join(’,’).split(’,’) } console.log(flatten2(arr)) //第三种稍有点问题,如果数组内是number类型会拍平后会变成字符串。2、写一个方法判断字符串内()是否成对出现,是返回true不是返回false let str = ‘(()()())’ let str1 = ‘(())()())’ //1、先用栈的思路解决 function isTure(str, result = []) { let arr = str.split(’’) for (let i = 0; i < arr.length; i++) { const item = arr[i]; // 如果是左括号直接压栈 if (item === ‘(’) { // 压栈 result.push(item); // 如果是右括号且当前arr不为空弹出栈顶 } else if (item === ‘)’ && result.length != 0) { // 弹出栈顶 result.pop() } else { //如果是右括号且当前result为空,则直接判定为不合法 return false } } return result ? true : false } console.log(isTure(str)) //true console.log(isTure(str1)) //false 2、用计数方式其实和栈原理类似 function isTure1(str, count = 0) { let arr = str.split(’’) for (let i = 0; i < arr.length; i++) { const item = arr[i]; if (item === ‘(’) { count++ } else if (item === ‘)’ && count != 0) { count– } else { return false } } return !count ? true : false } console.log(isTure1(str))//true console.log(isTure1(str1))//falseok 今天分享就到这,明天继续! ...

January 25, 2019 · 2 min · jiezi

Kube Controller Manager 源码分析

Kube Controller Manager 源码分析Controller Manager 在k8s 集群中扮演着中心管理的角色,它负责Deployment, StatefulSet, ReplicaSet 等资源的创建与管理,可以说是k8s的核心模块,下面我们以概略的形式走读一下k8s Controller Manager 代码。func NewControllerManagerCommand() *cobra.Command { s, err := options.NewKubeControllerManagerOptions() if err != nil { klog.Fatalf(“unable to initialize command options: %v”, err) } cmd := &cobra.Command{ Use: “kube-controller-manager”, Long: The Kubernetes controller manager is a daemon that embedsthe core control loops shipped with Kubernetes. In applications of robotics andautomation, a control loop is a non-terminating loop that regulates the state ofthe system. In Kubernetes, a controller is a control loop that watches the sharedstate of the cluster through the apiserver and makes changes attempting to move thecurrent state towards the desired state. Examples of controllers that ship withKubernetes today are the replication controller, endpoints controller, namespacecontroller, and serviceaccounts controller., Run: func(cmd *cobra.Command, args []string) { verflag.PrintAndExitIfRequested() utilflag.PrintFlags(cmd.Flags()) c, err := s.Config(KnownControllers(), ControllersDisabledByDefault.List()) if err != nil { fmt.Fprintf(os.Stderr, “%v\n”, err) os.Exit(1) } if err := Run(c.Complete(), wait.NeverStop); err != nil { fmt.Fprintf(os.Stderr, “%v\n”, err) os.Exit(1) } }, }Controller Manager 也是一个命令行,通过一系列flag启动,具体的各个flag 我们就不多看,有兴趣的可以去文档或者flags_opinion.go 文件里面去过滤一下,我们直接从Run 函数入手。Run Function 启动流程Kube Controller Manager 既可以单实例启动,也可以多实例启动。 如果为了保证 HA 而启动多个Controller Manager,它就需要选主来保证同一时间只有一个Master 实例。我们来看一眼Run 函数的启动流程,这里会把一些不重要的细节函数略过,只看重点func Run(c *config.CompletedConfig, stopCh <-chan struct{}) error { run := func(ctx context.Context) { rootClientBuilder := controller.SimpleControllerClientBuilder{ ClientConfig: c.Kubeconfig, } controllerContext, err := CreateControllerContext(c, rootClientBuilder, clientBuilder, ctx.Done()) if err != nil { klog.Fatalf(“error building controller context: %v”, err) } if err := StartControllers(controllerContext, saTokenControllerInitFunc, NewControllerInitializers(controllerContext.LoopMode), unsecuredMux); err != nil { klog.Fatalf(“error starting controllers: %v”, err) } controllerContext.InformerFactory.Start(controllerContext.Stop) close(controllerContext.InformersStarted) select {} } id, err := os.Hostname() if err != nil { return err } // add a uniquifier so that two processes on the same host don’t accidentally both become active id = id + “_” + string(uuid.NewUUID()) rl, err := resourcelock.New(c.ComponentConfig.Generic.LeaderElection.ResourceLock, “kube-system”, “kube-controller-manager”, c.LeaderElectionClient.CoreV1(), resourcelock.ResourceLockConfig{ Identity: id, EventRecorder: c.EventRecorder, }) if err != nil { klog.Fatalf(“error creating lock: %v”, err) } leaderelection.RunOrDie(context.TODO(), leaderelection.LeaderElectionConfig{ Lock: rl, LeaseDuration: c.ComponentConfig.Generic.LeaderElection.LeaseDuration.Duration, RenewDeadline: c.ComponentConfig.Generic.LeaderElection.RenewDeadline.Duration, RetryPeriod: c.ComponentConfig.Generic.LeaderElection.RetryPeriod.Duration, Callbacks: leaderelection.LeaderCallbacks{ OnStartedLeading: run, OnStoppedLeading: func() { klog.Fatalf(“leaderelection lost”) }, }, WatchDog: electionChecker, Name: “kube-controller-manager”, }) panic(“unreachable”)}这里的基本流程如下:首先定义了run 函数,run 函数负责具体的controller 构建以及最终的controller 操作的执行使用Client-go 提供的选主函数来进行选主如果获得主权限,那么就调用OnStartedLeading 注册函数,也就是上面的run 函数来执行操作,如果没选中,就hang住等待选主流程解析Client-go 选主工具类主要是通过kubeClient 在Configmap或者Endpoint选择一个资源创建,然后哪一个goroutine 创建成功了资源,哪一个goroutine 获得锁,当然所有的锁信息都会存在Configmap 或者Endpoint里面。之所以选择这两个资源类型,主要是考虑他们被Watch的少,但是现在kube Controller Manager 还是适用的Endpoint,后面会逐渐迁移到ConfigMap,因为Endpoint会被kube-proxy Ingress Controller等频繁Watch,我们来看一眼集群内Endpoint内容[root@iZ8vb5qgxqbxakfo1cuvpaZ ~]# kubectl get ep -n kube-system kube-controller-manager -o yamlapiVersion: v1kind: Endpointsmetadata: annotations: control-plane.alpha.kubernetes.io/leader: ‘{“holderIdentity”:“iZ8vbccmhgkyfdi8aii1hnZ_d880fea6-1322-11e9-913f-00163e033b49”,“leaseDurationSeconds”:15,“acquireTime”:“2019-01-08T08:53:49Z”,“renewTime”:“2019-01-22T11:16:59Z”,“leaderTransitions”:1}’ creationTimestamp: 2019-01-08T08:52:56Z name: kube-controller-manager namespace: kube-system resourceVersion: “2978183” selfLink: /api/v1/namespaces/kube-system/endpoints/kube-controller-manager uid: cade1b65-1322-11e9-9931-00163e033b49可以看到,这里面涵盖了当前Master ID,获取Master的时间,更新频率以及下一次更新时间。这一切最终还是靠ETCD 完成的选主。主要的选主代码如下func New(lockType string, ns string, name string, client corev1.CoreV1Interface, rlc ResourceLockConfig) (Interface, error) { switch lockType { case EndpointsResourceLock: return &EndpointsLock{ EndpointsMeta: metav1.ObjectMeta{ Namespace: ns, Name: name, }, Client: client, LockConfig: rlc, }, nil case ConfigMapsResourceLock: return &ConfigMapLock{ ConfigMapMeta: metav1.ObjectMeta{ Namespace: ns, Name: name, }, Client: client, LockConfig: rlc, }, nil default: return nil, fmt.Errorf(“Invalid lock-type %s”, lockType) }}StartController选主完毕后,就需要真正启动controller了,我们来看一下启动controller 的代码func StartControllers(ctx ControllerContext, startSATokenController InitFunc, controllers map[string]InitFunc, unsecuredMux *mux.PathRecorderMux) error { // Always start the SA token controller first using a full-power client, since it needs to mint tokens for the rest // If this fails, just return here and fail since other controllers won’t be able to get credentials. if _, _, err := startSATokenController(ctx); err != nil { return err } // Initialize the cloud provider with a reference to the clientBuilder only after token controller // has started in case the cloud provider uses the client builder. if ctx.Cloud != nil { ctx.Cloud.Initialize(ctx.ClientBuilder, ctx.Stop) } for controllerName, initFn := range controllers { if !ctx.IsControllerEnabled(controllerName) { klog.Warningf("%q is disabled", controllerName) continue } time.Sleep(wait.Jitter(ctx.ComponentConfig.Generic.ControllerStartInterval.Duration, ControllerStartJitter)) klog.V(1).Infof(“Starting %q”, controllerName) debugHandler, started, err := initFn(ctx) if err != nil { klog.Errorf(“Error starting %q”, controllerName) return err } if !started { klog.Warningf(“Skipping %q”, controllerName) continue } if debugHandler != nil && unsecuredMux != nil { basePath := “/debug/controllers/” + controllerName unsecuredMux.UnlistedHandle(basePath, http.StripPrefix(basePath, debugHandler)) unsecuredMux.UnlistedHandlePrefix(basePath+"/", http.StripPrefix(basePath, debugHandler)) } klog.Infof(“Started %q”, controllerName) } return nil}遍历所有的controller list执行每个controller 的Init Function那么一共有多少Controller 呢func NewControllerInitializers(loopMode ControllerLoopMode) map[string]InitFunc { controllers := map[string]InitFunc{} controllers[“endpoint”] = startEndpointController controllers[“replicationcontroller”] = startReplicationController controllers[“podgc”] = startPodGCController controllers[“resourcequota”] = startResourceQuotaController controllers[“namespace”] = startNamespaceController controllers[“serviceaccount”] = startServiceAccountController controllers[“garbagecollector”] = startGarbageCollectorController controllers[“daemonset”] = startDaemonSetController controllers[“job”] = startJobController controllers[“deployment”] = startDeploymentController controllers[“replicaset”] = startReplicaSetController controllers[“horizontalpodautoscaling”] = startHPAController controllers[“disruption”] = startDisruptionController controllers[“statefulset”] = startStatefulSetController controllers[“cronjob”] = startCronJobController controllers[“csrsigning”] = startCSRSigningController controllers[“csrapproving”] = startCSRApprovingController controllers[“csrcleaner”] = startCSRCleanerController controllers[“ttl”] = startTTLController controllers[“bootstrapsigner”] = startBootstrapSignerController controllers[“tokencleaner”] = startTokenCleanerController controllers[“nodeipam”] = startNodeIpamController controllers[“nodelifecycle”] = startNodeLifecycleController if loopMode == IncludeCloudLoops { controllers[“service”] = startServiceController controllers[“route”] = startRouteController controllers[“cloud-node-lifecycle”] = startCloudNodeLifecycleController // TODO: volume controller into the IncludeCloudLoops only set. } controllers[“persistentvolume-binder”] = startPersistentVolumeBinderController controllers[“attachdetach”] = startAttachDetachController controllers[“persistentvolume-expander”] = startVolumeExpandController controllers[“clusterrole-aggregation”] = startClusterRoleAggregrationController controllers[“pvc-protection”] = startPVCProtectionController controllers[“pv-protection”] = startPVProtectionController controllers[“ttl-after-finished”] = startTTLAfterFinishedController controllers[“root-ca-cert-publisher”] = startRootCACertPublisher return controllers}答案就在这里,上面的代码列出来了当前kube controller manager 所有的controller,既有大家熟悉的Deployment StatefulSet 也有一些不熟悉的身影。下面我们以Deployment 为例看看它到底干了什么Deployment Controller先来看一眼Deployemnt Controller 启动函数func startDeploymentController(ctx ControllerContext) (http.Handler, bool, error) { if !ctx.AvailableResources[schema.GroupVersionResource{Group: “apps”, Version: “v1”, Resource: “deployments”}] { return nil, false, nil } dc, err := deployment.NewDeploymentController( ctx.InformerFactory.Apps().V1().Deployments(), ctx.InformerFactory.Apps().V1().ReplicaSets(), ctx.InformerFactory.Core().V1().Pods(), ctx.ClientBuilder.ClientOrDie(“deployment-controller”), ) if err != nil { return nil, true, fmt.Errorf(“error creating Deployment controller: %v”, err) } go dc.Run(int(ctx.ComponentConfig.DeploymentController.ConcurrentDeploymentSyncs), ctx.Stop) return nil, true, nil}看到这里,如果看过上一篇针对Client-go Informer 文章的肯定不陌生,这里又使用了InformerFactory,而且是好几个。其实kube Controller Manager 里面大量使用了Informer,Controller 就是使用 Informer 来通知和观察所有的资源。可以看到,这里Deployment Controller 主要关注Deployment ReplicaSet Pod 这三个资源。Deployment Controller 资源初始化下面来看一下Deployemnt Controller 初始化需要的资源// NewDeploymentController creates a new DeploymentController.func NewDeploymentController(dInformer appsinformers.DeploymentInformer, rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, client clientset.Interface) (*DeploymentController, error) { eventBroadcaster := record.NewBroadcaster() eventBroadcaster.StartLogging(klog.Infof) eventBroadcaster.StartRecordingToSink(&v1core.EventSinkImpl{Interface: client.CoreV1().Events("")}) if client != nil && client.CoreV1().RESTClient().GetRateLimiter() != nil { if err := metrics.RegisterMetricAndTrackRateLimiterUsage(“deployment_controller”, client.CoreV1().RESTClient().GetRateLimiter()); err != nil { return nil, err } } dc := &DeploymentController{ client: client, eventRecorder: eventBroadcaster.NewRecorder(scheme.Scheme, v1.EventSource{Component: “deployment-controller”}), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), “deployment”), } dc.rsControl = controller.RealRSControl{ KubeClient: client, Recorder: dc.eventRecorder, } dInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: dc.addDeployment, UpdateFunc: dc.updateDeployment, // This will enter the sync loop and no-op, because the deployment has been deleted from the store. DeleteFunc: dc.deleteDeployment, }) rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: dc.addReplicaSet, UpdateFunc: dc.updateReplicaSet, DeleteFunc: dc.deleteReplicaSet, }) podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ DeleteFunc: dc.deletePod, }) dc.syncHandler = dc.syncDeployment dc.enqueueDeployment = dc.enqueue dc.dLister = dInformer.Lister() dc.rsLister = rsInformer.Lister() dc.podLister = podInformer.Lister() dc.dListerSynced = dInformer.Informer().HasSynced dc.rsListerSynced = rsInformer.Informer().HasSynced dc.podListerSynced = podInformer.Informer().HasSynced return dc, nil}是不是这里的代码似曾相识,如果接触过Client-go Informer 的代码,可以看到这里如出一辙,基本上就是对创建的资源分别触发对应的Add Update Delete 函数,同时所有的资源通过Lister获得,不需要真正的Query APIServer。先来看一下针对Deployment 的Handlerfunc (dc *DeploymentController) addDeployment(obj interface{}) { d := obj.(*apps.Deployment) klog.V(4).Infof(“Adding deployment %s”, d.Name) dc.enqueueDeployment(d)}func (dc *DeploymentController) updateDeployment(old, cur interface{}) { oldD := old.(*apps.Deployment) curD := cur.(*apps.Deployment) klog.V(4).Infof(“Updating deployment %s”, oldD.Name) dc.enqueueDeployment(curD)}func (dc *DeploymentController) deleteDeployment(obj interface{}) { d, ok := obj.(*apps.Deployment) if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { utilruntime.HandleError(fmt.Errorf(“Couldn’t get object from tombstone %#v”, obj)) return } d, ok = tombstone.Obj.(*apps.Deployment) if !ok { utilruntime.HandleError(fmt.Errorf(“Tombstone contained object that is not a Deployment %#v”, obj)) return } } klog.V(4).Infof(“Deleting deployment %s”, d.Name) dc.enqueueDeployment(d)}不论是Add Update Delete,处理方法如出一辙,都是一股脑的塞到Client-go 提供的worker Queue里面。 再来看看ReplicaSetfunc (dc *DeploymentController) addReplicaSet(obj interface{}) { rs := obj.(*apps.ReplicaSet) if rs.DeletionTimestamp != nil { // On a restart of the controller manager, it’s possible for an object to // show up in a state that is already pending deletion. dc.deleteReplicaSet(rs) return } // If it has a ControllerRef, that’s all that matters. if controllerRef := metav1.GetControllerOf(rs); controllerRef != nil { d := dc.resolveControllerRef(rs.Namespace, controllerRef) if d == nil { return } klog.V(4).Infof(“ReplicaSet %s added.”, rs.Name) dc.enqueueDeployment(d) return } // Otherwise, it’s an orphan. Get a list of all matching Deployments and sync // them to see if anyone wants to adopt it. ds := dc.getDeploymentsForReplicaSet(rs) if len(ds) == 0 { return } klog.V(4).Infof(“Orphan ReplicaSet %s added.”, rs.Name) for _, d := range ds { dc.enqueueDeployment(d) }}func (dc *DeploymentController) updateReplicaSet(old, cur interface{}) { curRS := cur.(*apps.ReplicaSet) oldRS := old.(*apps.ReplicaSet) if curRS.ResourceVersion == oldRS.ResourceVersion { // Periodic resync will send update events for all known replica sets. // Two different versions of the same replica set will always have different RVs. return } curControllerRef := metav1.GetControllerOf(curRS) oldControllerRef := metav1.GetControllerOf(oldRS) controllerRefChanged := !reflect.DeepEqual(curControllerRef, oldControllerRef) if controllerRefChanged && oldControllerRef != nil { // The ControllerRef was changed. Sync the old controller, if any. if d := dc.resolveControllerRef(oldRS.Namespace, oldControllerRef); d != nil { dc.enqueueDeployment(d) } } // If it has a ControllerRef, that’s all that matters. if curControllerRef != nil { d := dc.resolveControllerRef(curRS.Namespace, curControllerRef) if d == nil { return } klog.V(4).Infof(“ReplicaSet %s updated.”, curRS.Name) dc.enqueueDeployment(d) return } // Otherwise, it’s an orphan. If anything changed, sync matching controllers // to see if anyone wants to adopt it now. labelChanged := !reflect.DeepEqual(curRS.Labels, oldRS.Labels) if labelChanged || controllerRefChanged { ds := dc.getDeploymentsForReplicaSet(curRS) if len(ds) == 0 { return } klog.V(4).Infof(“Orphan ReplicaSet %s updated.”, curRS.Name) for _, d := range ds { dc.enqueueDeployment(d) } }}总结一下Add 和 Update根据ReplicaSet ownerReferences 寻找到对应的Deployment Name判断是否Rs 发生了变化如果变化就把Deployment 塞到Wokrer Queue里面去最后看一下针对Pod 的处理func (dc *DeploymentController) deletePod(obj interface{}) { pod, ok := obj.(*v1.Pod) // When a delete is dropped, the relist will notice a pod in the store not // in the list, leading to the insertion of a tombstone object which contains // the deleted key/value. Note that this value might be stale. If the Pod // changed labels the new deployment will not be woken up till the periodic resync. if !ok { tombstone, ok := obj.(cache.DeletedFinalStateUnknown) if !ok { utilruntime.HandleError(fmt.Errorf(“Couldn’t get object from tombstone %#v”, obj)) return } pod, ok = tombstone.Obj.(*v1.Pod) if !ok { utilruntime.HandleError(fmt.Errorf(“Tombstone contained object that is not a pod %#v”, obj)) return } } klog.V(4).Infof(“Pod %s deleted.”, pod.Name) if d := dc.getDeploymentForPod(pod); d != nil && d.Spec.Strategy.Type == apps.RecreateDeploymentStrategyType { // Sync if this Deployment now has no more Pods. rsList, err := util.ListReplicaSets(d, util.RsListFromClient(dc.client.AppsV1())) if err != nil { return } podMap, err := dc.getPodMapForDeployment(d, rsList) if err != nil { return } numPods := 0 for _, podList := range podMap { numPods += len(podList.Items) } if numPods == 0 { dc.enqueueDeployment(d) } }}可以看到,基本思路差不多,当检查到Deployment 所有的Pod 都被删除后,将Deployment name 塞到Worker Queue 里面去。Deployment Controller Run 函数资源初始化完毕后,就开始真正的Run 来看一下Run 函数func (dc *DeploymentController) Run(workers int, stopCh <-chan struct{}) { defer utilruntime.HandleCrash() defer dc.queue.ShutDown() klog.Infof(“Starting deployment controller”) defer klog.Infof(“Shutting down deployment controller”) if !controller.WaitForCacheSync(“deployment”, stopCh, dc.dListerSynced, dc.rsListerSynced, dc.podListerSynced) { return } for i := 0; i < workers; i++ { go wait.Until(dc.worker, time.Second, stopCh) } <-stopCh}func (dc *DeploymentController) worker() { for dc.processNextWorkItem() { }}func (dc *DeploymentController) processNextWorkItem() bool { key, quit := dc.queue.Get() if quit { return false } defer dc.queue.Done(key) err := dc.syncHandler(key.(string)) dc.handleErr(err, key) return true}可以看到 这个代码就是Client-go 里面标准版的Worker 消费者,不断的从Queue 里面拿Obj 然后调用syncHandler 处理,一起来看看最终的Handler如何处理dc.syncHandlerfunc (dc *DeploymentController) syncDeployment(key string) error { startTime := time.Now() klog.V(4).Infof(“Started syncing deployment %q (%v)”, key, startTime) defer func() { klog.V(4).Infof(“Finished syncing deployment %q (%v)”, key, time.Since(startTime)) }() namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } deployment, err := dc.dLister.Deployments(namespace).Get(name) if errors.IsNotFound(err) { klog.V(2).Infof(“Deployment %v has been deleted”, key) return nil } if err != nil { return err } // Deep-copy otherwise we are mutating our cache. // TODO: Deep-copy only when needed. d := deployment.DeepCopy() everything := metav1.LabelSelector{} if reflect.DeepEqual(d.Spec.Selector, &everything) { dc.eventRecorder.Eventf(d, v1.EventTypeWarning, “SelectingAll”, “This deployment is selecting all pods. A non-empty selector is required.”) if d.Status.ObservedGeneration < d.Generation { d.Status.ObservedGeneration = d.Generation dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(d) } return nil } // List ReplicaSets owned by this Deployment, while reconciling ControllerRef // through adoption/orphaning. rsList, err := dc.getReplicaSetsForDeployment(d) if err != nil { return err } // List all Pods owned by this Deployment, grouped by their ReplicaSet. // Current uses of the podMap are: // // * check if a Pod is labeled correctly with the pod-template-hash label. // * check that no old Pods are running in the middle of Recreate Deployments. podMap, err := dc.getPodMapForDeployment(d, rsList) if err != nil { return err } if d.DeletionTimestamp != nil { return dc.syncStatusOnly(d, rsList) } // Update deployment conditions with an Unknown condition when pausing/resuming // a deployment. In this way, we can be sure that we won’t timeout when a user // resumes a Deployment with a set progressDeadlineSeconds. if err = dc.checkPausedConditions(d); err != nil { return err } if d.Spec.Paused { return dc.sync(d, rsList) } // rollback is not re-entrant in case the underlying replica sets are updated with a new // revision so we should ensure that we won’t proceed to update replica sets until we // make sure that the deployment has cleaned up its rollback spec in subsequent enqueues. if getRollbackTo(d) != nil { return dc.rollback(d, rsList) } scalingEvent, err := dc.isScalingEvent(d, rsList) if err != nil { return err } if scalingEvent { return dc.sync(d, rsList) } switch d.Spec.Strategy.Type { case apps.RecreateDeploymentStrategyType: return dc.rolloutRecreate(d, rsList, podMap) case apps.RollingUpdateDeploymentStrategyType: return dc.rolloutRolling(d, rsList) } return fmt.Errorf(“unexpected deployment strategy type: %s”, d.Spec.Strategy.Type)}根据Worker Queue 取出来的Namespace & Name 从Lister 内Query到真正的Deployment 对象根据Deployment label 查询对应的ReplicaSet 列表根据ReplicaSet label 查询对应的 Pod 列表,并生成一个key 为ReplicaSet ID Value 为PodList的Map 数据结构判断当前Deployment 是否处于暂停状态判断当前Deployment 是否处于回滚状态根据更新策略Recreate 还是 RollingUpdate 决定对应的动作这里我们以Recreate为例来看一下策略动作func (dc *DeploymentController) rolloutRecreate(d *apps.Deployment, rsList []*apps.ReplicaSet, podMap map[types.UID]*v1.PodList) error { // Don’t create a new RS if not already existed, so that we avoid scaling up before scaling down. newRS, oldRSs, err := dc.getAllReplicaSetsAndSyncRevision(d, rsList, false) if err != nil { return err } allRSs := append(oldRSs, newRS) activeOldRSs := controller.FilterActiveReplicaSets(oldRSs) // scale down old replica sets. scaledDown, err := dc.scaleDownOldReplicaSetsForRecreate(activeOldRSs, d) if err != nil { return err } if scaledDown { // Update DeploymentStatus. return dc.syncRolloutStatus(allRSs, newRS, d) } // Do not process a deployment when it has old pods running. if oldPodsRunning(newRS, oldRSs, podMap) { return dc.syncRolloutStatus(allRSs, newRS, d) } // If we need to create a new RS, create it now. if newRS == nil { newRS, oldRSs, err = dc.getAllReplicaSetsAndSyncRevision(d, rsList, true) if err != nil { return err } allRSs = append(oldRSs, newRS) } // scale up new replica set. if _, err := dc.scaleUpNewReplicaSetForRecreate(newRS, d); err != nil { return err } if util.DeploymentComplete(d, &d.Status) { if err := dc.cleanupDeployment(oldRSs, d); err != nil { return err } } // Sync deployment status. return dc.syncRolloutStatus(allRSs, newRS, d)}根据ReplicaSet 获取当前所有的新老ReplicaSet如果有老的ReplicaSet 那么先把老的ReplicaSet replicas 缩容设置为0,当然第一次创建的时候是没有老ReplicaSet的如果第一次创建,那么需要去创建对应的ReplicaSet创建完毕对应的ReplicaSet后 扩容ReplicaSet 到对应的值等待新建的创建完毕,清理老的ReplcaiSet更新Deployment Status下面我们看看第一次创建Deployment 的代码func (dc *DeploymentController) getNewReplicaSet(d *apps.Deployment, rsList, oldRSs []*apps.ReplicaSet, createIfNotExisted bool) (*apps.ReplicaSet, error) { existingNewRS := deploymentutil.FindNewReplicaSet(d, rsList) // Calculate the max revision number among all old RSes maxOldRevision := deploymentutil.MaxRevision(oldRSs) // Calculate revision number for this new replica set newRevision := strconv.FormatInt(maxOldRevision+1, 10) // Latest replica set exists. We need to sync its annotations (includes copying all but // annotationsToSkip from the parent deployment, and update revision, desiredReplicas, // and maxReplicas) and also update the revision annotation in the deployment with the // latest revision. if existingNewRS != nil { rsCopy := existingNewRS.DeepCopy() // Set existing new replica set’s annotation annotationsUpdated := deploymentutil.SetNewReplicaSetAnnotations(d, rsCopy, newRevision, true) minReadySecondsNeedsUpdate := rsCopy.Spec.MinReadySeconds != d.Spec.MinReadySeconds if annotationsUpdated || minReadySecondsNeedsUpdate { rsCopy.Spec.MinReadySeconds = d.Spec.MinReadySeconds return dc.client.AppsV1().ReplicaSets(rsCopy.ObjectMeta.Namespace).Update(rsCopy) } // Should use the revision in existingNewRS’s annotation, since it set by before needsUpdate := deploymentutil.SetDeploymentRevision(d, rsCopy.Annotations[deploymentutil.RevisionAnnotation]) // If no other Progressing condition has been recorded and we need to estimate the progress // of this deployment then it is likely that old users started caring about progress. In that // case we need to take into account the first time we noticed their new replica set. cond := deploymentutil.GetDeploymentCondition(d.Status, apps.DeploymentProgressing) if deploymentutil.HasProgressDeadline(d) && cond == nil { msg := fmt.Sprintf(“Found new replica set %q”, rsCopy.Name) condition := deploymentutil.NewDeploymentCondition(apps.DeploymentProgressing, v1.ConditionTrue, deploymentutil.FoundNewRSReason, msg) deploymentutil.SetDeploymentCondition(&d.Status, *condition) needsUpdate = true } if needsUpdate { var err error if d, err = dc.client.AppsV1().Deployments(d.Namespace).UpdateStatus(d); err != nil { return nil, err } } return rsCopy, nil } if !createIfNotExisted { return nil, nil } // new ReplicaSet does not exist, create one. newRSTemplate := *d.Spec.Template.DeepCopy() podTemplateSpecHash := controller.ComputeHash(&newRSTemplate, d.Status.CollisionCount) newRSTemplate.Labels = labelsutil.CloneAndAddLabel(d.Spec.Template.Labels, apps.DefaultDeploymentUniqueLabelKey, podTemplateSpecHash) // Add podTemplateHash label to selector. newRSSelector := labelsutil.CloneSelectorAndAddLabel(d.Spec.Selector, apps.DefaultDeploymentUniqueLabelKey, podTemplateSpecHash) // Create new ReplicaSet newRS := apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{ // Make the name deterministic, to ensure idempotence Name: d.Name + “-” + podTemplateSpecHash, Namespace: d.Namespace, OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(d, controllerKind)}, Labels: newRSTemplate.Labels, }, Spec: apps.ReplicaSetSpec{ Replicas: new(int32), MinReadySeconds: d.Spec.MinReadySeconds, Selector: newRSSelector, Template: newRSTemplate, }, } allRSs := append(oldRSs, &newRS) newReplicasCount, err := deploymentutil.NewRSNewReplicas(d, allRSs, &newRS) if err != nil { return nil, err } *(newRS.Spec.Replicas) = newReplicasCount // Set new replica set’s annotation deploymentutil.SetNewReplicaSetAnnotations(d, &newRS, newRevision, false) // Create the new ReplicaSet. If it already exists, then we need to check for possible // hash collisions. If there is any other error, we need to report it in the status of // the Deployment. alreadyExists := false createdRS, err := dc.client.AppsV1().ReplicaSets(d.Namespace).Create(&newRS)这里截取了部分重要代码首先查询一下当前是否有对应的新的ReplicaSet如果有那么仅仅需要更新Deployment Status 即可如果没有 那么创建对应的ReplicaSet 结构体最后调用Client-go 创建对应的ReplicaSet 实例后面还有一些代码 这里就不贴了,核心思想就是,根据ReplicaSet的情况创建对应的新的ReplicaSet,其实看到使用Client-go 创建ReplicaSet Deployment 这里基本完成了使命,剩下的就是根据watch 改变一下Deployment 的状态了,至于真正的Pod 的创建,那么就得ReplicaSet Controller 来完成了。ReplicaSet ControllerReplicaSet Controller 和Deployment Controller 长得差不多,重复的部分我们就不多说,先看一下初始化的时候,ReplicaSet 主要关注哪些资源func NewBaseController(rsInformer appsinformers.ReplicaSetInformer, podInformer coreinformers.PodInformer, kubeClient clientset.Interface, burstReplicas int, gvk schema.GroupVersionKind, metricOwnerName, queueName string, podControl controller.PodControlInterface) *ReplicaSetController { if kubeClient != nil && kubeClient.CoreV1().RESTClient().GetRateLimiter() != nil { metrics.RegisterMetricAndTrackRateLimiterUsage(metricOwnerName, kubeClient.CoreV1().RESTClient().GetRateLimiter()) } rsc := &ReplicaSetController{ GroupVersionKind: gvk, kubeClient: kubeClient, podControl: podControl, burstReplicas: burstReplicas, expectations: controller.NewUIDTrackingControllerExpectations(controller.NewControllerExpectations()), queue: workqueue.NewNamedRateLimitingQueue(workqueue.DefaultControllerRateLimiter(), queueName), } rsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.enqueueReplicaSet, UpdateFunc: rsc.updateRS, // This will enter the sync loop and no-op, because the replica set has been deleted from the store. // Note that deleting a replica set immediately after scaling it to 0 will not work. The recommended // way of achieving this is by performing a stop operation on the replica set. DeleteFunc: rsc.enqueueReplicaSet, }) rsc.rsLister = rsInformer.Lister() rsc.rsListerSynced = rsInformer.Informer().HasSynced podInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{ AddFunc: rsc.addPod, // This invokes the ReplicaSet for every pod change, eg: host assignment. Though this might seem like // overkill the most frequent pod update is status, and the associated ReplicaSet will only list from // local storage, so it should be ok. UpdateFunc: rsc.updatePod, DeleteFunc: rsc.deletePod, }) rsc.podLister = podInformer.Lister() rsc.podListerSynced = podInformer.Informer().HasSynced rsc.syncHandler = rsc.syncReplicaSet return rsc}可以看到ReplicaSet Controller 主要关注所有的ReplicaSet Pod的创建,他们的处理逻辑是一样的,都是根据触发函数,找到对应的ReplicaSet实例后,将对应的ReplicaSet 实例放到Worker Queue里面去。syncReplicaSet这里我们直接来看ReplicaSet Controller 的真正处理函数func (rsc *ReplicaSetController) syncReplicaSet(key string) error { startTime := time.Now() defer func() { klog.V(4).Infof(“Finished syncing %v %q (%v)”, rsc.Kind, key, time.Since(startTime)) }() namespace, name, err := cache.SplitMetaNamespaceKey(key) if err != nil { return err } rs, err := rsc.rsLister.ReplicaSets(namespace).Get(name) if errors.IsNotFound(err) { klog.V(4).Infof("%v %v has been deleted", rsc.Kind, key) rsc.expectations.DeleteExpectations(key) return nil } if err != nil { return err } rsNeedsSync := rsc.expectations.SatisfiedExpectations(key) selector, err := metav1.LabelSelectorAsSelector(rs.Spec.Selector) if err != nil { utilruntime.HandleError(fmt.Errorf(“Error converting pod selector to selector: %v”, err)) return nil } // list all pods to include the pods that don’t match the rs`s selector // anymore but has the stale controller ref. // TODO: Do the List and Filter in a single pass, or use an index. allPods, err := rsc.podLister.Pods(rs.Namespace).List(labels.Everything()) if err != nil { return err } // Ignore inactive pods. var filteredPods []*v1.Pod for _, pod := range allPods { if controller.IsPodActive(pod) { filteredPods = append(filteredPods, pod) } } // NOTE: filteredPods are pointing to objects from cache - if you need to // modify them, you need to copy it first. filteredPods, err = rsc.claimPods(rs, selector, filteredPods) if err != nil { return err } var manageReplicasErr error if rsNeedsSync && rs.DeletionTimestamp == nil { manageReplicasErr = rsc.manageReplicas(filteredPods, rs) } rs = rs.DeepCopy() newStatus := calculateStatus(rs, filteredPods, manageReplicasErr)根据从Worker Queue 得到的Name 获取真正的ReplicaSet 实例根据ReplicaSet Label 获取对应的所有的Pod List将所有的Running Pod 遍历出来根据Pod 情况判断是否需要创建 Pod将新的状态更新到ReplicaSet Status 字段中manageReplicas我们主要来看一眼创建Pod 的函数func (rsc *ReplicaSetController) manageReplicas(filteredPods []*v1.Pod, rs apps.ReplicaSet) error { diff := len(filteredPods) - int((rs.Spec.Replicas)) rsKey, err := controller.KeyFunc(rs) if err != nil { utilruntime.HandleError(fmt.Errorf(“Couldn’t get key for %v %#v: %v”, rsc.Kind, rs, err)) return nil } if diff < 0 { diff *= -1 if diff > rsc.burstReplicas { diff = rsc.burstReplicas } // TODO: Track UIDs of creates just like deletes. The problem currently // is we’d need to wait on the result of a create to record the pod’s // UID, which would require locking across the create, which will turn // into a performance bottleneck. We should generate a UID for the pod // beforehand and store it via ExpectCreations. rsc.expectations.ExpectCreations(rsKey, diff) klog.V(2).Infof(“Too few replicas for %v %s/%s, need %d, creating %d”, rsc.Kind, rs.Namespace, rs.Name, *(rs.Spec.Replicas), diff) // Batch the pod creates. Batch sizes start at SlowStartInitialBatchSize // and double with each successful iteration in a kind of “slow start”. // This handles attempts to start large numbers of pods that would // likely all fail with the same error. For example a project with a // low quota that attempts to create a large number of pods will be // prevented from spamming the API service with the pod create requests // after one of its pods fails. Conveniently, this also prevents the // event spam that those failures would generate. successfulCreations, err := slowStartBatch(diff, controller.SlowStartInitialBatchSize, func() error { boolPtr := func(b bool) *bool { return &b } controllerRef := &metav1.OwnerReference{ APIVersion: rsc.GroupVersion().String(), Kind: rsc.Kind, Name: rs.Name, UID: rs.UID, BlockOwnerDeletion: boolPtr(true), Controller: boolPtr(true), } err := rsc.podControl.CreatePodsWithControllerRef(rs.Namespace, &rs.Spec.Template, rs, controllerRef) if err != nil && errors.IsTimeout(err) { // Pod is created but its initialization has timed out. // If the initialization is successful eventually, the // controller will observe the creation via the informer. // If the initialization fails, or if the pod keeps // uninitialized for a long time, the informer will not // receive any update, and the controller will create a new // pod when the expectation expires. return nil } return err }) // Any skipped pods that we never attempted to start shouldn’t be expected. // The skipped pods will be retried later. The next controller resync will // retry the slow start process. if skippedPods := diff - successfulCreations; skippedPods > 0 { klog.V(2).Infof(“Slow-start failure. Skipping creation of %d pods, decrementing expectations for %v %v/%v”, skippedPods, rsc.Kind, rs.Namespace, rs.Name) for i := 0; i < skippedPods; i++ { // Decrement the expected number of creates because the informer won’t observe this pod rsc.expectations.CreationObserved(rsKey) } } return err } else if diff > 0 { if diff > rsc.burstReplicas { diff = rsc.burstReplicas } klog.V(2).Infof(“Too many replicas for %v %s/%s, need %d, deleting %d”, rsc.Kind, rs.Namespace, rs.Name, *(rs.Spec.Replicas), diff) // Choose which Pods to delete, preferring those in earlier phases of startup. podsToDelete := getPodsToDelete(filteredPods, diff) // Snapshot the UIDs (ns/name) of the pods we’re expecting to see // deleted, so we know to record their expectations exactly once either // when we see it as an update of the deletion timestamp, or as a delete. // Note that if the labels on a pod/rs change in a way that the pod gets // orphaned, the rs will only wake up after the expectations have // expired even if other pods are deleted. rsc.expectations.ExpectDeletions(rsKey, getPodKeys(podsToDelete)) errCh := make(chan error, diff) var wg sync.WaitGroup wg.Add(diff) for _, pod := range podsToDelete { go func(targetPod *v1.Pod) { defer wg.Done() if err := rsc.podControl.DeletePod(rs.Namespace, targetPod.Name, rs); err != nil { // Decrement the expected number of deletes because the informer won’t observe this deletion podKey := controller.PodKey(targetPod) klog.V(2).Infof(“Failed to delete %v, decrementing expectations for %v %s/%s”, podKey, rsc.Kind, rs.Namespace, rs.Name) rsc.expectations.DeletionObserved(rsKey, podKey) errCh <- err } }(pod) } wg.Wait()这里的逻辑就非常简单的,基本上就是根据当前Running Pod 数量和真正的replicas 声明比对,如果少了那么就调用Client-go 创建Pod ,如果多了就调用CLient-go 去删除 Pod。总结至此,一个Deployment -> ReplicaSet -> Pod 就真正的创建完毕。当Pod 被删除时候,ReplicaSet Controller 就会把 Pod 拉起来。如果更新Deployment 就会创建新的ReplicaSet 一层层嵌套多个Controller 结合完成最终的 Pod 创建。 当然,这里其实仅仅完成了Pod 数据写入到ETCD,其实真正的 Pod 实例并没有创建,还需要scheduler & kubelet 配合完成,我们会在后面的章节继续介绍。本文作者:xianlubird阅读原文本文为云栖社区原创内容,未经允许不得转载。 ...

January 23, 2019 · 18 min · jiezi

[LeetCode] 165. Compare Version Numbers

ProblemCompare two version numbers version1 and version2.If version1 > version2 return 1; if version1 < version2 return -1;otherwise return 0.You may assume that the version strings are non-empty and contain only digits and the . character.The . character does not represent a decimal point and is used to separate number sequences.For instance, 2.5 is not “two and a half” or “half way to version three”, it is the fifth second-level revision of the second first-level revision.You may assume the default revision number for each level of a version number to be 0. For example, version number 3.4 has a revision number of 3 and 4 for its first and second level revision number. Its third and fourth level revision number are both 0.Example 1:Input: version1 = “0.1”, version2 = “1.1"Output: -1Example 2:Input: version1 = “1.0.1”, version2 = “1"Output: 1Example 3:Input: version1 = “7.5.2.4”, version2 = “7.5.3"Output: -1Example 4:Input: version1 = “1.01”, version2 = “1.001"Output: 0Explanation: Ignoring leading zeroes, both “01” and “001” represent the same number “1”Example 5:Input: version1 = “1.0”, version2 = “1.0.0"Output: 0Explanation: The first version number does not have a third level revision number, which means its third level revision number is default to “0"Note:Version strings are composed of numeric strings separated by dots . and this numeric strings may have leading zeroes.Version strings do not start or end with dots, and they will not be two consecutive dots.Solutionclass Solution { public int compareVersion(String version1, String version2) { String[] v1 = version1.split(”\.”); String[] v2 = version2.split(”\.”); int len = Math.max(v1.length, v2.length); for (int i = 0; i < len; i++) { int n1 = i < v1.length ? Integer.parseInt(v1[i]) : 0; int n2 = i < v2.length ? Integer.parseInt(v2[i]) : 0; int ans = Integer.compare(n1, n2); if (ans != 0) return ans; } return 0; }} ...

January 14, 2019 · 2 min · jiezi

[LeetCode] 51. N-Queens

ProblemThe n-queens puzzle is the problem of placing n queens on an n×n chessboard such that no two queens attack each other.Given an integer n, return all distinct solutions to the n-queens puzzle.Each solution contains a distinct board configuration of the n-queens’ placement, where ‘Q’ and ‘.’ both indicate a queen and an empty space respectively.Example:Input: 4Output: [ [".Q..", // Solution 1 “…Q”, “Q…”, “..Q.”], ["..Q.", // Solution 2 “Q…”, “…Q”, “.Q..”]]Explanation: There exist two distinct solutions to the 4-queens puzzle as shown above.Solutionclass Solution { public List<List<String>> solveNQueens(int n) { char[][] board = new char[n][n]; for (int i = 0; i < n; i++) { Arrays.fill(board[i], ‘.’); } List<List<String>> res = new ArrayList<>(); dfs(board, 0, res); return res; } private void dfs(char[][] board, int col, List<List<String>> res) { if (col == board.length) { res.add(construct(board)); return; } for (int row = 0; row < board.length; row++) { if (validate(board, row, col)) { board[row][col] = ‘Q’; dfs(board, col+1, res); board[row][col] = ‘.’; } } } private boolean validate(char[][] board, int row, int col) { for (int i = 0; i < board.length; i++) { for (int j = 0; j < col; j++) { if (board[i][j] == ‘Q’ && ( i+j == row+col || row+j == col+i || row == i )) return false; } } return true; } private List<String> construct(char[][] board) { List<String> res = new ArrayList<>(); for (int i = 0; i < board.length; i++) { String str = new String(board[i]); res.add(str); } return res; }} ...

December 30, 2018 · 2 min · jiezi

[LeetCode] 249. Group Shifted Strings

ProblemGiven a string, we can “shift” each of its letter to its successive letter, for example: “abc” -> “bcd”. We can keep “shifting” which forms the sequence:“abc” -> “bcd” -> … -> “xyz"Given a list of strings which contains only lowercase alphabets, group all strings that belong to the same shifting sequence.Example:Input: [“abc”, “bcd”, “acef”, “xyz”, “az”, “ba”, “a”, “z”],Output: [ [“abc”,“bcd”,“xyz”], [“az”,“ba”], [“acef”], [“a”,“z”]]Solutionclass Solution { public List<List<String>> groupStrings(String[] strings) { Map<String, List<String>> map = new HashMap<>(); for (String str: strings) { StringBuilder sb = new StringBuilder(); for (int i = 1; i < str.length(); i++) { int diff = str.charAt(i)-str.charAt(i-1); if (diff < 0) diff += 26; sb.append(‘a’+diff); //***** ‘a’ ***** } String key = sb.toString(); map.putIfAbsent(key, new ArrayList<>()); map.get(key).add(str); } return new ArrayList<>(map.values()); }}//[“an”,“bcf”]//keys: 110, 98100//result: [[“an”],[“bcf”]] ...

December 30, 2018 · 1 min · jiezi

[LeetCode] 332. Reconstruct Itinerary

ProblemGiven a list of airline tickets represented by pairs of departure and arrival airports [from, to], reconstruct the itinerary in order. All of the tickets belong to a man who departs from JFK. Thus, the itinerary must begin with JFK.Note:If there are multiple valid itineraries, you should return the itinerary that has the smallest lexical order when read as a single string. For example, the itinerary [“JFK”, “LGA”] has a smaller lexical order than [“JFK”, “LGB”].All airports are represented by three capital letters (IATA code).You may assume all tickets form at least one valid itinerary.Example 1:Input: [[“MUC”, “LHR”], [“JFK”, “MUC”], [“SFO”, “SJC”], [“LHR”, “SFO”]]Output: [“JFK”, “MUC”, “LHR”, “SFO”, “SJC”]Example 2:Input: [[“JFK”,“SFO”],[“JFK”,“ATL”],[“SFO”,“ATL”],[“ATL”,“JFK”],[“ATL”,“SFO”]]Output: [“JFK”,“ATL”,“JFK”,“SFO”,“ATL”,“SFO”]Explanation: Another possible reconstruction is [“JFK”,“SFO”,“ATL”,“JFK”,“ATL”,“SFO”]. But it is larger in lexical order.Solutionclass Solution { public List<String> findItinerary(String[][] tickets) { Map<String, PriorityQueue<String>> map = new HashMap<>(); List<String> res = new ArrayList<>(); for (String[] ticket: tickets) { if (!map.containsKey(ticket[0])) map.put(ticket[0], new PriorityQueue<>()); map.get(ticket[0]).add(ticket[1]); } dfs(“JFK”, map, res); return res; } public void dfs(String departure, Map<String, PriorityQueue<String>> map, List<String> res) { PriorityQueue<String> arrivals = map.get(departure); while (arrivals != null && arrivals.size() > 0) { dfs(arrivals.poll(), map, res); } res.add(0, departure); }} ...

December 30, 2018 · 1 min · jiezi

leetcode讲解--811. Subdomain Visit Count

题目A website domain like “discuss.leetcode.com” consists of various subdomains. At the top level, we have “com”, at the next level, we have “leetcode.com”, and at the lowest level, “discuss.leetcode.com”. When we visit a domain like “discuss.leetcode.com”, we will also visit the parent domains “leetcode.com” and “com” implicitly.Now, call a “count-paired domain” to be a count (representing the number of visits this domain received), followed by a space, followed by the address. An example of a count-paired domain might be “9001 discuss.leetcode.com”.We are given a list cpdomains of count-paired domains. We would like a list of count-paired domains, (in the same format as the input, and in any order), that explicitly counts the number of visits to each subdomain.Example 1:Input: [“9001 discuss.leetcode.com”]Output: [“9001 discuss.leetcode.com”, “9001 leetcode.com”, “9001 com”]Explanation: We only have one website domain: “discuss.leetcode.com”. As discussed above, the subdomain “leetcode.com” and “com” will also be visited. So they will all be visited 9001 times.Example 2:Input: [“900 google.mail.com”, “50 yahoo.com”, “1 intel.mail.com”, “5 wiki.org”]Output: [“901 mail.com”,“50 yahoo.com”,“900 google.mail.com”,“5 wiki.org”,“5 org”,“1 intel.mail.com”,“951 com”]Explanation: We will visit “google.mail.com” 900 times, “yahoo.com” 50 times, “intel.mail.com” once and “wiki.org” 5 times. For the subdomains, we will visit “mail.com” 900 + 1 = 901 times, “com” 900 + 50 + 1 = 951 times, and “org” 5 times.Notes:The length of cpdomains will not exceed 100.The length of each domain name will not exceed 100.Each address will have either 1 or 2 “.” characters.The input count in any count-paired domain will not exceed 10000.The answer output can be returned in any order.题目地址讲解这道题我反了一个小错误,然后检查的时候都没检查出来。java代码class Solution { public List<String> subdomainVisits(String[] cpdomains) { Map<String, Integer> map = new HashMap<>(); List<String> result = new ArrayList<>(); for(String s:cpdomains){ int indexOfSpace = s.indexOf(’ ‘); Integer count = map.get(s.substring(indexOfSpace+1)); Integer add = Integer.parseInt(s.substring(0,indexOfSpace)); if(count==null){ map.put(s.substring(indexOfSpace+1), add); }else{ map.put(s.substring(indexOfSpace+1), count+add); } List<Integer> indexOfPoint = new ArrayList<>(); for(int i=s.length()-1;i>indexOfSpace;i–){ if(s.charAt(i)==’.’){ indexOfPoint.add(i); } } for(int i=0;i<indexOfPoint.size();i++){ String domainName = s.substring(indexOfPoint.get(i)+1); count = map.get(domainName); if(count==null){ map.put(domainName, add); }else{ map.put(domainName, count+add); } } } for(String key:map.keySet()){ result.add(map.get(key)+" “+key); } return result; }} ...

December 27, 2018 · 2 min · jiezi

[LeetCode] 767. Reorganize String

ProblemGiven a string S, check if the letters can be rearranged so that two characters that are adjacent to each other are not the same.If possible, output any possible result. If not possible, return the empty string.Example 1:Input: S = “aab"Output: “aba"Example 2:Input: S = “aaab"Output: ““Note:S will consist of lowercase letters and have length in range [1, 500].Solutionclass Solution { public String reorganizeString(String S) { int n = S.length(); int[] cnt = new int[128]; char mc = ‘a’; for (char c : S.toCharArray()) { cnt[c]++; mc = (cnt[c] > cnt[mc]) ? c : mc; } if (cnt[mc] == 1) { return S; } if (cnt[mc] > (n+1)/2) { return “”; } StringBuilder[] sb = new StringBuilder[cnt[mc]]; for (int i = 0; i < sb.length; i ++) { sb[i] = new StringBuilder(); sb[i].append(mc); } int k = 0; for (char c = ‘a’; c <= ‘z’; c++) { while (c != mc && cnt[c] > 0) { sb[k++].append(c); cnt[c]–; k %= sb.length; } } for (int i = 1; i < sb.length; i++) { sb[0].append(sb[i]); } return sb[0].toString(); }} ...

December 27, 2018 · 1 min · jiezi

[LeetCode] 402. Remove K Digits

ProblemGiven a non-negative integer num represented as a string, remove k digits from the number so that the new number is the smallest possible.Note:The length of num is less than 10002 and will be ≥ k.The given num does not contain any leading zero.Example 1:Input: num = “1432219”, k = 3Output: “1219"Explanation: Remove the three digits 4, 3, and 2 to form the new number 1219 which is the smallest.Example 2:Input: num = “10200”, k = 1Output: “200"Explanation: Remove the leading 1 and the number is 200. Note that the output must not contain leading zeroes.Example 3:Input: num = “10”, k = 2Output: “0"Explanation: Remove all the digits from the number and it is left with nothing which is 0.Solutionclass Solution { public String removeKdigits(String num, int k) { Deque<Character> stack = new ArrayDeque<>(); int n = num.length(); if (k >= n) return “0”; for (int i = 0; i < n; i++) { while (k > 0 && !stack.isEmpty() && stack.peek() > num.charAt(i)) { stack.pop(); k–; } stack.push(num.charAt(i)); } while (k > 0) { stack.pop(); k–; } StringBuilder sb = new StringBuilder(); while (!stack.isEmpty()) sb.append(stack.pop()); sb.reverse(); while (sb.length() > 1 && sb.charAt(0) == ‘0’) sb.deleteCharAt(0); return sb.toString(); }} ...

December 26, 2018 · 1 min · jiezi

[LeetCode] 722. Remove Comments

ProblemGiven a C++ program, remove comments from it. The program source is an array where source[i] is the i-th line of the source code. This represents the result of splitting the original source code string by the newline character n.In C++, there are two types of comments, line comments, and block comments.The string // denotes a line comment, which represents that it and rest of the characters to the right of it in the same line should be ignored.The string / denotes a block comment, which represents that all characters until the next (non-overlapping) occurrence of / should be ignored. (Here, occurrences happen in reading order: line by line from left to right.) To be clear, the string // does not yet end the block comment, as the ending would be overlapping the beginning.The first effective comment takes precedence over others: if the string // occurs in a block comment, it is ignored. Similarly, if the string / occurs in a line or block comment, it is also ignored.If a certain line of code is empty after removing comments, you must not output that line: each string in the answer list will be non-empty.There will be no control characters, single quote, or double quote characters. For example, source = “string s = “/ Not a comment. /”;” will not be a test case. (Also, nothing else such as defines or macros will interfere with the comments.)It is guaranteed that every open block comment will eventually be closed, so /* outside of a line or block comment always starts a new comment.Finally, implicit newline characters can be deleted by block comments. Please see the examples below for details.After removing the comments from the source code, return the source code in the same format.Example 1:Input: source = ["/Test program /", “int main()”, “{ “, " // variable declaration “, “int a, b, c;”, “/ This is a test”, " multiline “, " comment for “, " testing /”, “a = b + c;”, “}"]The line by line code is visualized as below:/Test program /int main(){ // variable declaration int a, b, c;/* This is a test multiline comment for testing /a = b + c;}Output:[“int main()”,”{ “,” “,“int a, b, c;”,“a = b + c;”,”}"]The line by line code is visualized as below:int main(){ int a, b, c;a = b + c;}Explanation:The string / denotes a block comment, including line 1 and lines 6-9. The string // denotes line 4 as comments.Example 2:Input: source = [“a/comment”, “line”, “more_comment/b”]Output:[“ab”]Explanation: The original source string is “a/commentnlinenmore_comment/b”, where we have bolded the newline characters. After deletion, the implicit newline characters are deleted, leaving the string “ab”, which when delimited by newline characters becomes [“ab”].Solutionclass Solution { public List<String> removeComments(String[] source) { List<String> res = new ArrayList<>(); StringBuilder sb = new StringBuilder(); boolean comment = false; for (String s: source) { for (int i = 0; i < s.length(); i++) { if (!comment) { if (s.charAt(i) == ‘/’ && i != s.length()-1 && s.charAt(i+1) == ‘/’) break; else if (s.charAt(i) == ‘/’ && i != s.length()-1 && s.charAt(i+1) == ‘’) { comment = true; i++; } else sb.append(s.charAt(i)); } else { if (s.charAt(i) == ‘’ && i < s.length()-1 && s.charAt(i+1) == ‘/’) { comment = false; i++; } } } if (!comment && sb.length() > 0) { res.add(sb.toString()); sb.setLength(0); } } return res; }} ...

December 26, 2018 · 3 min · jiezi

[LeetCode] 657. Robot Return to Origin

ProblemThere is a robot starting at position (0, 0), the origin, on a 2D plane. Given a sequence of its moves, judge if this robot ends up at (0, 0) after it completes its moves.The move sequence is represented by a string, and the character moves[i] represents its ith move. Valid moves are R (right), L (left), U (up), and D (down). If the robot returns to the origin after it finishes all of its moves, return true. Otherwise, return false.Note: The way that the robot is “facing” is irrelevant. “R” will always make the robot move to the right once, “L” will always make it move left, etc. Also, assume that the magnitude of the robot’s movement is the same for each move.Example 1:Input: “UD"Output: true Explanation: The robot moves up once, and then down once. All moves have the same magnitude, so it ended up at the origin where it started. Therefore, we return true.Example 2:Input: “LL"Output: falseExplanation: The robot moves left twice. It ends up two “moves” to the left of the origin. We return false because it is not at the origin at the end of its moves.Solutionclass Solution { public boolean judgeCircle(String moves) { if (moves == null || moves.length() == 0) return true; int x = 0, y = 0; int i = 0; while (i < moves.length()) { char ch = moves.charAt(i); if (ch == ‘U’) y++; else if (ch == ‘D’) y–; else if (ch == ‘L’) x–; else if (ch == ‘R’) x++; else return false; i++; } return x == 0 && y == 0; }} ...

December 25, 2018 · 2 min · jiezi

[LeetCode] 71. Simplify Path

ProblemGiven an absolute path for a file (Unix-style), simplify it. For example,path = “/home/”, => “/home"path = “/a/./b/../../c/”, => “/c"path = “/a/../../b/../c//.//”, => “/c” //here: b is cancelled out, a is cancelled outpath = “/a//b////c/d//././/..”, => “/a/b/c” //here d is cancelled outIn a UNIX-style file system, a period (’.’) refers to the current directory, so it can be ignored in a simplified path. Additionally, a double period (”..”) moves up a directory, so it cancels out whatever the last directory was. For more information, look here: https://en.wikipedia.org/wiki...Corner Cases:Did you consider the case where path = “/../"?In this case, you should return “/".Another corner case is the path might contain multiple slashes ‘/’ together, such as “/home//foo/".In this case, you should ignore redundant slashes and return “/home/foo”.Solutionclass Solution { public String simplifyPath(String path) { Deque<String> stack = new ArrayDeque<>(); Set<String> ignore = new HashSet<>(Arrays.asList(””, “.”, “..”)); for (String dir: path.split(”/")) { if (!stack.isEmpty() && dir.equals("..")) stack.pop(); if (!ignore.contains(dir)) stack.push(dir); } String res = “”; while (!stack.isEmpty()) { res = “/"+stack.pop()+res; } if (res.equals(”")) return “/”; return res; }} ...

December 17, 2018 · 1 min · jiezi

关于String内的indexOf方法的一些疑问

今天浏览了一下java里的String类,发现一个静态方法有点意思,就是我们常用的indexOf(String str)的底层实现,先看下代码调用链。public int indexOf(String str) { return indexOf(str, 0);} public int indexOf(String str, int fromIndex) { return indexOf(value, 0, value.length, str.value, 0, str.value.length, fromIndex);}static int indexOf(char[] source, int sourceOffset, int sourceCount, String target, int fromIndex) { return indexOf(source, sourceOffset, sourceCount, target.value, 0, target.value.length, fromIndex);}/** * Code shared by String and StringBuffer to do searches. The * source is the character array being searched, and the target * is the string being searched for. * * @param source the characters being searched. * @param sourceOffset offset of the source string. * @param sourceCount count of the source string. * @param target the characters being searched for. * @param targetOffset offset of the target string. * @param targetCount count of the target string. * @param fromIndex the index to begin searching from. /static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) { if (fromIndex >= sourceCount) { return (targetCount == 0 ? sourceCount : -1); } if (fromIndex < 0) { fromIndex = 0; } if (targetCount == 0) { return fromIndex; } char first = target[targetOffset]; int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) { / Look for first character. / if (source[i] != first) { while (++i <= max && source[i] != first); } / Found first character, now look at the rest of v2 / if (i <= max) { int j = i + 1; int end = j + targetCount - 1; for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++); if (j == end) { / Found whole string. / return i - sourceOffset; } } } return -1;}底层的字符串匹配的逻辑比较简单,就是普通的匹配模式:查找首字符,匹配target的第一个字符在source内的位置,若查找到max位置还找到,则返回-1;若在source匹配到了target的第一个字符,那么在依次比较srouce和target后面的字符,一直到target的末尾;如果target后面的字符与source都已经匹配,则返回在source上匹配到的第一个字符的相对下标,否则返回-1。但是仔细读代码会发现一个问题,就是这里int max = sourceOffset + (sourceCount - targetCount);max的计算方式,max的作用是计算出最大的首字符匹配次数,取值范围应该是"max <= sourceCount"。所以target字符串的长度是可以不用匹配的,故“sourceCount - targetCount”是没问题的。关键的地方是这里加上了sourceOffset,sourceOffset是source字符串的起始匹配偏移量,即从source的哪个字符开始匹配。所以,根据代码里的max计算方式,最终计算出来的max值是会有可能大于sourceCount。看下测试代码:package string;/* * string test /public class StringTest { static int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex) { if (fromIndex >= sourceCount) { return (targetCount == 0 ? sourceCount : -1); } if (fromIndex < 0) { fromIndex = 0; } if (targetCount == 0) { return fromIndex; } char first = target[targetOffset]; int max = sourceOffset + (sourceCount - targetCount); for (int i = sourceOffset + fromIndex; i <= max; i++) { / Look for first character. / if (source[i] != first) { while (++i <= max && source[i] != first); } / Found first character, now look at the rest of v2 / if (i <= max) { int j = i + 1; int end = j + targetCount - 1; for (int k = targetOffset + 1; j < end && source[j] == target[k]; j++, k++); if (j == end) { / Found whole string. */ return i - sourceOffset; } } } return -1; } public static void main(String[] args) { String source = “abcdefghigklmn”; String target = “n”; int sourceOffset = 5; int targetOffset = 0; int index = indexOf(source.toCharArray(), sourceOffset, source.length(), target.toCharArray(), targetOffset, target.length(), 0); System.out.println(index); }}如果target在source内可以匹配到返回正确结果8(结果8是相对于sourceOffset的结果,如果转换成source内的位置则是13)。但是如果target在source内匹配不到,则会抛出java.lang.ArrayIndexOutOfBoundsException异常,如下:Exception in thread “main” java.lang.ArrayIndexOutOfBoundsException: 14 at string.StringTest.indexOf(StringTest.java:27) at string.StringTest.main(StringTest.java:52)可见报出越界的下标是14,这就是由于max = sourceOffset + (sourceCount - targetCount)引起,计算出的max值为:17。所以,个人认为max计算这里是个潜在的BUG,应该改为 int max = sourceCount - targetCount;不过这个方法是一个非public方法,只在String内部调用,同时也跟踪了所有对该方法的调用链,都是传入的默认0,在使用时不会出现数组越界问题。不知这是开发者故意为之,还是其它我未知用意,欢迎大家交流讨论!!! ...

December 14, 2018 · 3 min · jiezi

[LeetCode] 243. Shortest Word Distance

ProblemGiven a list of words and two words word1 and word2, return the shortest distance between these two words in the list.Example:Assume that words = [“practice”, “makes”, “perfect”, “coding”, “makes”].Input: word1 = “coding”, word2 = “practice”Output: 3Input: word1 = “makes”, word2 = “coding"Output: 1Note:You may assume that word1 does not equal to word2, and word1 and word2 are both in the list.Solutionclass Solution { public int shortestDistance(String[] words, String word1, String word2) { if (word1.equals(word2)) return 0; int m = -1, n = -1; int min = words.length; for (int i = 0; i < words.length; i++) { if (words[i].equals(word1)) { m = i; if (n != -1) min = Math.min(min, m-n); } else if (words[i].equals(word2)) { n = i; if (m != -1) min = Math.min(min, n-m); } } if (m == -1 || n == -1) return -1; return min; }} ...

December 14, 2018 · 1 min · jiezi

leetcode383. Ransom Note

题目Given an arbitrary ransom note string and another string containing letters from all the magazines, write a function that will return true if the ransom note can be constructed from the magazines ; otherwise, it will return false.Each letter in the magazine string can only be used once in your ransom note.Note:You may assume that both strings contain only lowercase letters.canConstruct(“a”, “b”) -> falsecanConstruct(“aa”, “ab”) -> falsecanConstruct(“aa”, “aab”) -> true假设有一组字母和一组从杂志中获取的字母,问是否能够用从杂志中获取的字母构成想要的那组字母,要求每个单词只能使用一次。思路一使用索引为字母的数组来存储该字母还剩下几个没有从杂志中找到。每从杂志中找到一个字母,对应字母位置上的数字减一,每遇到一个字母则该字母位置上的数字加一。如果没有多余的字母,则说明可以找到足够多的字母拼接。 public boolean canConstruct(String ransomNote, String magazine) { if(ransomNote.isEmpty()) return true; if(ransomNote.length() > magazine.length()) return false; int p1 = 0, p2 = 0; int[] count = new int[26]; int wordCount = 0; while(p1 < ransomNote.length() && p2 < ransomNote.length()) { char c1 = ransomNote.charAt(p1); char c2 = magazine.charAt(p2); if(++count[c1-‘a’] == 1) { wordCount++; } if(–count[c2-‘a’] == 0) { wordCount–; } p1++; p2++; } while(p2 < magazine.length()) { if(wordCount == 0) break; char c = magazine.charAt(p2); count[c-‘a’]–; if(count[c-‘a’] == 0) { wordCount–; } p2++; } return wordCount == 0; }思路二:找不到字母就结束思路二利用java的API来查找magazine中从上一个找到的字母开始,下一个字母所在的位置。如果找不到,则说明无法完成拼接。 public boolean canConstruct(String ransomNote, String magazine) { int len=ransomNote.length(); int[] index = new int[128]; for(int i = 0; i < len; i++){ char cu=ransomNote.charAt(i); int result=magazine.indexOf(cu,index[cu]); if(result == -1){ return false; } else{ index[cu]=result + 1; } } return true; } ...

November 23, 2018 · 1 min · jiezi

寻找Java中String.split性能更好的方法

String.split 是Java里很常用的字符串操作,在普通业务操作里使用的话并没有什么问题,但如果需要追求高性能的分割的话,需要花一点心思找出可以提高性能的方法。String.split方法的分割参数regex实际不是字符串,而是正则表达式,就是说分隔字符串支持按正则进行分割,虽然这个特性看上去非常好,但从另一个角度来说也是性能杀手。在Java6的实现里,String.split每次调用都直接新建Pattern对象对参数进行正则表达式的编译,再进行字符串分隔,而正则表达式的编译从字面上看就知道需要耗不少时间,并且实现中也没有对Pattern进行缓存,因此多次频繁调用的使用场景下性能很差,如果是要使用正则表达式分隔的话,应该自行对Pattern进行缓存。public String[] split(String regex, int limit) { return Pattern.compile(regex).split(this, limit);}但很多时候我们并不会真的想使用正则表达式分隔字符串,我们其实想的只是用一个简单的字符比如空格、下划线分隔字符串而已,为了需要是满足这个需求却要背上正则表达式支持的性能损耗,非常不值得。因此在Java7的实现里,针对单字符的分隔进行了优化,对这种场景实现了更合适的方法。单字符不走正则表达式的实现,直接利用indexOf快速定位分隔位置,提高性能。/* fastpath if the regex is a (1)one-char String and this character is not one of the RegEx’s meta characters “.$|()[{^?+\”, or (2)two-char String and the first char is the backslash and the second is not the ascii digit or ascii letter./char ch = 0;if (((regex.value.length == 1 && “.$|()[{^?*+\".indexOf(ch = regex.charAt(0)) == -1) || (regex.length() == 2 && regex.charAt(0) == ‘\’ && (((ch = regex.charAt(1))-‘0’)|(‘9’-ch)) < 0 && ((ch-‘a’)|(‘z’-ch)) < 0 && ((ch-‘A’)|(‘Z’-ch)) < 0)) && (ch < Character.MIN_HIGH_SURROGATE || ch > Character.MAX_LOW_SURROGATE)){ int off = 0; int next = 0; boolean limited = limit > 0; ArrayList<String> list = new ArrayList<>(); while ((next = indexOf(ch, off)) != -1) { if (!limited || list.size() < limit - 1) { list.add(substring(off, next)); off = next + 1; } else { // last one //assert (list.size() == limit - 1); list.add(substring(off, value.length)); off = value.length; break; } } // If no match was found, return this if (off == 0) return new String[]{this}; // Add remaining segment if (!limited || list.size() < limit) list.add(substring(off, value.length)); // Construct result int resultSize = list.size(); if (limit == 0) while (resultSize > 0 && list.get(resultSize - 1).length() == 0) resultSize–; String[] result = new String[resultSize]; return list.subList(0, resultSize).toArray(result);}有没有更快的方法?如果分隔符不是单字符而且也不需要按正则分隔的话,使用split的方法还会和Java6一样使用正则表达式。这里还有其他备用手段:使用StringTokenizer,StringTokenizer没有正则表达式分隔的功能,单纯的根据分隔符逐次返回分隔的子串,默认按空格分隔,性能比String.split方法稍好,但这个类实现比较老,属于jdk的遗留类,而且注释上也说明不建议使用这个类。使用org.apache.commons.lang3.StringUtils.split分隔字符串,针对不需要按正则分隔的场景提供更好的实现,分隔符支持字符串。还能有更快的方法么?注意到String.split和StringUtils.split方法返回值是String[], 原始数组的大小是固定的,而在分隔字符串不可能提前知道分隔了多少个子串,那这个数组肯定藏了猫腻,看看是怎么实现的。定位String.split单字符实现,发现分隔的子串其实保存在ArrayList里,并没有高深的技巧,直到路径的最后一行,代码对存储了子串的ArrayList再转成数组,而toArray的实现里对数组进行了复制。return list.subList(0, resultSize).toArray(result);StringUtils.split方法里同样也是这样。return list.toArray(new String[list.size()]);因此这里可以做一个优化,把代码实现复制过来,然后将方法参数返回类型改为List,减少数组复制的内存消耗。还能有更快的方法么?其实很多时候我们需要对分隔后的字符串进行遍历访问做一些操作,并不是真的需要这个数组,这和文件读取是一样的道理,读文件不需要把整个文件读入到内存中再使用,完全可以一次读取一行进行处理,因此还可以做一个优化,增加参数作为子串处理方法的回调,在相应地方改为对回调的调用,这样能完全避免数组的创建。也就是说,把字符串分隔看做一个流。private static void splitWorker(final String str, final String separatorChars, final int max, final boolean preserveAllTokens, Consumer<String> onSplit) { if (str == null) { return; } final int len = str.length(); if (len == 0) { return; } int sizePlus1 = 1; int i = 0, start = 0; boolean match = false; boolean lastMatch = false; if (separatorChars == null) { // Null separator means use whitespace while (i < len) { if (Character.isWhitespace(str.charAt(i))) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } else if (separatorChars.length() == 1) { // Optimise 1 character case final char sep = separatorChars.charAt(0); while (i < len) { if (str.charAt(i) == sep) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } else { // standard case while (i < len) { if (separatorChars.indexOf(str.charAt(i)) >= 0) { if (match || preserveAllTokens) { lastMatch = true; if (sizePlus1++ == max) { i = len; lastMatch = false; } onSplit.accept(str.substring(start, i)); match = false; } start = ++i; continue; } lastMatch = false; match = true; i++; } } if (match || preserveAllTokens && lastMatch) { onSplit.accept(str.substring(start, i)); }}public static void split(final String str, final String separatorChars, Consumer<String> onSplit) { splitWorker(str, separatorChars, -1, false, onSplit);}// 使用方法public void example() { split(“Hello world”, " “, System.out::println);}还能有更快的方法么?也有更极端的优化方法,因为在拿子串(substring方法)时实际发生了一次字符串复制,因此可以把回调函数改为传入子串在字符串的区间start、end,回调再根据区间读取子串进行处理,但并不是很通用,这里就不展示代码了,有兴趣的可以试一下。还能有更快的方… ...

November 4, 2018 · 3 min · jiezi

【go源码分析】strings.go 里的那些骚操作

go version go1.11 darwin/amd64/src/strings/strings.gostrings.go 文件中定义了近40个常用的字符串操作函数(公共函数)。以下是主要的几个函数。函数简介Index(s, substr string) int返回 substr 在 s 中第一次出现的位置,不存在返回 -1;采用RabinKarp算法Split(s, sep string) []string根据 sep 把字符串 s 进行切分,返回切分后的数组Join(a []string, sep string) string跟 Split 功能刚好相反Repeat(s string, count int) string返回字符串 s 重复 count 次得到的字符串Trim(s string, cutset string) string返回去除首尾存在于 cutset 的字符的切片Replace(s, old, new string, n int) string字符串替换EqualFold(s, t string) bool判断两个字符串代表的文件夹是否相等(忽略大小写)以及 ToUpper ToLower Title(Title 函数把单词转换成标题形式,不是ToTitle)。还有一些以上函数派生出的其他函数。比如:Contains 基本是通过 Index 函数实现的;与 Index 原理一致的 LastIndex 函数;与 Trim 有关的 TrimLeft TrimRight 等。接下来,本文会对 Index Trim Join Repeat Replace 函数进行分析。ps: len 返回的是字符串的字节数,不是字符数。字符数请使用 utf8.RuneCountInStringIndex: RabinKarp 算法实现Index(s, substr string) int, 返回 substr 在 s 中第一次出现的位置,不存在返回 -1;采用RabinKarp算法Index 函数会先对 substr 的长度 n 进行判断,对特殊情况做快速处理。其次,如果长度 n 以及 len(s) 足够小,则使用BruteForce算法:即暴力匹配,拿 substr 与 s[i:i+n] 进行比较,如果相等,返回 i,其中 i = (from 0 to len(s) - n)…最后,会先尝试暴力匹配,如果匹配失败次数超过临界点,则换成 RabinKarp 算法。(为了方便阅读,文中不放全部代码,只展示核心代码与部分结构代码)Indexfunc Index(s, substr string) int { n := len(substr) // len 返回的是字节数 switch { case n == 0: return 0 case n == 1: // substr 是单字节字符串,则直接单字节进行比较 return IndexByte(s, substr[0]) case n == len(s): if substr == s { return 0 } return -1 case n > len(s): return -1 case n <= bytealg.MaxLen: // Use brute force when s and substr both are small if len(s) <= bytealg.MaxBruteForce { return bytealg.IndexString(s, substr) } // 这里有一大段省略的代码 // 循环尝试 substr == s[i:i+n] // 如果失败次数过多,则使用 bytealg.IndexString(s, substr) } // 这里有一大段省略的代码 // 循环尝试 substr == s[i:i+n] // 如果失败次数过多,则使用 indexRabinKarp(s[i:], substr) t := s[:len(s)-n+1] for i < len(t) { // … 省略代码 // 如果失败次数过多,则使用 RabinKarp 算法 if fails >= 4+i>>4 && i < len(t) { // 这里使用 s[i:] 作为参数 // 是因为前面的 s[:i] 都已经尝试过了 j := indexRabinKarp(s[i:], substr) if j < 0 { return -1 } return i + j } } return -1}在看 indexRabinKarp 函数之前,我们先了解一下 RabinKarp 算法。RobinKarp算法是由 Robin 和 Karp 提出的字符串匹配算法。该算法在实际应用中有较好的表现。算法的核心步骤:const primeRK = 16777619 // 大素数对 substr 构造 hash 值。 n = len(substr),hash = (substr[0]pow(primeRK, n-1) + substr[1]pow(primeRK, n-2) + … + substr[n-1]pow(primeRK, 0)) % anotherBiggerPrime对 s 的每 n 个子串按照相同逻辑构造 hash 值,判断与 substr 的 hash 是否相等;如果 hash 相等,则比较子串是否真的与 substr 相等重复第三步,直到找到,或者未找到。ps:该算法之所以快,是因为 s[i+1, i+n+1] 的 hash 值可以由 s[i, i+n] 的 hash 值计算出。即h(i+1) = ((h(i) - s[i] * pow(primeRK, n-1)) * primeRK + s[i+n+1]) % anotherBiggerPrime另外,go 计算 hash 时并没有 % anotherBiggerPrime,而是定义了 hash 为 uint32 类型,利用整型溢出实现了对 2**32 取模的效果。(一般来说是对另一个大素数取模,显然这里不是,不过毕竟这么大的数也没啥大影响)该算法预处理时间为 O(n),n 为 len(substr),运行最坏时间为 O((n-m+1)m),m 为 len(s)。最坏情况为每个子串的 hash 都与 substr 的一样。在平均情况下,运行时间还是很好的。除了 RabinKarp 算法外,还要一些其他的字符串匹配算法。《算法》导论中介绍了另外两种优秀的算法,分别是 有限自动机 与 Knuth-Morris-Pratt 算法(即 KMP 算法),这两个算法的运行时间都为 O(m)。下面是 indexRabinKarp 函数indexRabinKarpfunc indexRabinKarp(s, substr string) int { // Rabin-Karp search // hashss 是 substr 根据上述方法计算出的 hash 值 // pow 是 primeRK 的 len(substr) 次幂 hashss, pow := hashStr(substr) n := len(substr) // 计算 s[:n] 的 hash 值 var h uint32 for i := 0; i < n; i++ { h = hprimeRK + uint32(s[i]) } if h == hashss && s[:n] == substr { return 0 } for i := n; i < len(s); { // 计算下一个子串的 hash 值 h = primeRK h += uint32(s[i]) h -= pow * uint32(s[i-n]) i++ // 如果 hash 相等 且子串相等,则返回对应下标 if h == hashss && s[i-n:i] == substr { return i - n } } return -1}hashStr 函数跟计算 s[:n] 的 逻辑一致。不过不得不提一下 pow 的计算方法。hashStrfunc hashStr(sep string) (uint32, uint32) { hash := uint32(0) for i := 0; i < len(sep); i++ { hash = hashprimeRK + uint32(sep[i]) } // 下面我用 python 的乘方元素符 ** 代表乘方 // 我们已 len(sep) 为 6 为例来看此函数 // 6 的二进制是 110 // 每次循环,pow 和 sq 分别为 // i: 110 pow: 1 sq: rk // i: 11 pow: 1 sq: rk ** 2 // i: 1 pow: 1 * (rk ** 2) sq: rk ** 4 // i: 0 pow: 1 (rk ** 2) * (rk ** 4) sq: rk ** 8 // pow: 1 (rk ** 2) * (rk ** 4) = 1 * (rk ** 6) 即是 pow(rk, 6) var pow, sq uint32 = 1, primeRK for i := len(sep); i > 0; i >>= 1 { if i&1 != 0 { pow *= sq } sq *= sq } return hash, pow}以上是 Index 函数的实现逻辑。Trim: 出神入化的位操作Trim(s string, cutset string) string 返回去除首尾存在于 cutset 的字符的切片。执行 fmt.Println(strings.Trim(“hello world”, “hld”))输出 ello worTrim的本质逻辑也比较简单:根据 cutset 构造一个函数,该函数签名为 func(rune) bool,接受一个 rune类型的值,返回该值是否在 cutset 中然后调用 TrimLeft TrimRight;这两个函数调用了 indexFunc,其逻辑也比较简单,不再赘述函数 makeCutsetFunc(cutset string) func(rune) bool 就是刚才提到的构造 判断 rune 值是否在 cutset 中的函数 的函数。makeCutsetFuncfunc makeCutsetFunc(cutset string) func(rune) bool { // 如果 cutset 是单个字符,则直接返回一个简单函数, // 该函数判断入参 r 是否与 cutset[0] 相等 if len(cutset) == 1 && cutset[0] < utf8.RuneSelf { return func(r rune) bool { return r == rune(cutset[0]) } } // 如果 cutset 全是 ASCII 码 // 则使用构造的 as (asciiSet类型)判断 rune 是否在 cutset 中 if as, isASCII := makeASCIISet(cutset); isASCII { return func(r rune) bool { return r < utf8.RuneSelf && as.contains(byte(r)) } } // 调用 IndexRune 方法判断 r 是否在 cutset 中 // IndexRune 其实就是 Index 的变种 return func(r rune) bool { return IndexRune(cutset, r) >= 0 }}其中,最有意思的要数 makeASCIISet 函数,该函数用了一个 [8]uint32 数组实现了 128 个 ASCII 码的 hash 表。asciiSet// asciiSet 一共 32 个字节,一共 256 位,// 其中低 128 位分别代表了 128 个 ascii 码type asciiSet [8]uint32// makeASCIISet creates a set of ASCII characters and reports whether all// characters in chars are ASCII.func makeASCIISet(chars string) (as asciiSet, ok bool) { for i := 0; i < len(chars); i++ { c := chars[i] // const utf8.RuneSelf = 0x80 // 小于 utf8.RuneSelf 的值是 ASCII 码 // 大于 utf8.RuneSelf 的值是其他 utf8 字符的部分 if c >= utf8.RuneSelf { return as, false } // ASCII 的范围是 0000 0000 - 0111 1111 // c >> 5 的范围是 000 - 011,即最大为 3 // 31 的二进制是 0001 1111 // 1 << uint(c&31) 的结果刚好也在 uint 范围内 as[c>>5] |= 1 << uint(c&31) } return as, true}// contains reports whether c is inside the set.// 为了兼容入参 c 为 byte 类型, c >> 5 < 8// 所以 asciiSet 类型为 [8]uint32,数组长度为 8// 否则如果只考虑 128 个 ascii 码的话,[4]uint32 就够了func (as *asciiSet) contains(c byte) bool { return (as[c>>5] & (1 << uint(c&31))) != 0}以上是 Trim 函数及其位操作。从 Join Repeat Replace 看字符串如何生成这三个函数的逻辑都很简单,不再赘述。频繁申请内存是很耗费时间的,所以在生成某个字符串时,如果能够预知字符串的长度,就能直接申请对应长度的内存,然后调用 copy(dst, src []Type) int 函数把字符复制到对应位置,最后把 []byte 强转成字符串类型即可。Joinfunc Join(a []string, sep string) string { // 省略了部分特殊情况处理的代码 // 计算目标字符串总长度 n := len(sep) * (len(a) - 1) for i := 0; i < len(a); i++ { n += len(a[i]) } // 申请内存 b := make([]byte, n) bp := copy(b, a[0]) // 复制内容 for _, s := range a[1:] { bp += copy(b[bp:], sep) bp += copy(b[bp:], s) } // 返回数据 return string(b)}Repeatfunc Repeat(s string, count int) string { // 特殊情况处理 if count < 0 { panic(“strings: negative Repeat count”) } else if count > 0 && len(s)*count/count != len(s) { panic(“strings: Repeat count causes overflow”) } b := make([]byte, len(s)*count) bp := copy(b, s) // 2倍,4倍,8倍的扩大,直到 bp 不小于目标长度 for bp < len(b) { copy(b[bp:], b[:bp]) bp = 2 } return string(b)}Replacefunc Replace(s, old, new string, n int) string { // 省略一下特殊情况代码 // 计算新字符串的长度 t := make([]byte, len(s)+n(len(new)-len(old))) w := 0 start := 0 for i := 0; i < n; i++ { j := start if len(old) == 0 { if i > 0 { _, wid := utf8.DecodeRuneInString(s[start:]) j += wid } } else { j += Index(s[start:], old) } // 查找旧字符串的位置,复制 w += copy(t[w:], s[start:j]) w += copy(t[w:], new) start = j + len(old) } w += copy(t[w:], s[start:]) return string(t[0:w])}以上是关于生成字符串时避免多次分配内存的高效做法。Y_xx ...

September 29, 2018 · 6 min · jiezi