白菜不是菜

Welcome to my blog !


讲清楚CAS的那点事

Published at July 16, 2019 ·  1 min read

互联网低潮,老是会看到别人发面试经验,看到很多人谈乐观锁,谈CAS,但是都没有说清楚。忍不住叨叨几句。 那什么是乐观锁呢,比较书面的定义是 “它假设多用户并发的事务在处理时不会彼此互相影响,各事务能够在不产生锁的情况下处理各自影响的那部分数据。在提交数据更新之前,每个事务会先检查在该事务读取数据后,有没有其他事务又修改了该数据。如果其他事务有更新的话,正在提交的事务会进行回滚。”,在多线程中则指对于数据冲突保持一种乐观态度,操作数据时不会对操作的数据进行加锁(这使得多个任务可以并行的对数据进行操作),只有到数据提交的时候才通过一种机制来验证数据是否存在冲突。在Java中,是通过CAS来实现乐观锁的。 CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。 更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。 CAS比较难于理解的地方就在于V、A、B这三个变量到底代表什么含义,很容易混淆,也不容易讲清楚。单纯看书上的文章会觉得晦涩,我们可以来看个例子。 举个例子: 1.在内存地址V当中,存储着值为10的变量 2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。 3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。 4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare),发现A不等于V的实际值,提交失败。 5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋。 6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的,线程1进行Swap,把地址V的值替换为B,也就是12。在这一步,Compare和Swap这个过程是原子的(由操作系统和硬件保证),比较并更新的过程是不会被其他线程打断的。 到这里,其实基本说请了CAS的过程,但是CAS的API,还是不够清晰,很多人能够进行到这里,但如果让他实际使用CAS的API时则又没辙了,这里我们通过一个例子来演示CAS的API实际用例。 /** * 使用CAS来获取单例 */ public class CasSingleton { private static final AtomicReference<CasSingleton> INSTANCE=new AtomicReference<>(); private CasSingleton(){} public static CasSingleton getInstance(){ for(;;){ CasSingleton singleton=INSTANCE.get(); if(null!=singleton){ return singleton; } singleton=new CasSingleton(); if(INSTANCE.compareAndSet(null,singleton)){ return singleton; } } } public static void main(String[] args) { CasSingleton singleton=getInstance(); CasSingleton singleton1=getInstance(); System.out.println(singleton); System.out.println(singleton1); } } 这里使用CAS来实现单例,我们对照着compareAndSet的API来看看 /** * Atomically sets the value to {@code newValue} * if the current value {@code == expectedValue}, * with memory effects as specified by {@link VarHandle#compareAndSet}....

Netty实现http服务器keep Alive无效的问题排查

Published at July 6, 2019 ·  2 min read

netty实现http服务器keep-alive无效的问题排查 今天在用netty实现一个http服务器的时候,发现keep-alive并没有生效,具体表现是在request和response的header里都能看到keep-alive的标志: request: Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3 Accept-Encoding: gzip, deflate, br Accept-Language: zh-CN,zh;q=0.9,en;q=0.8 Cache-Control: max-age=0 Connection: keep-alive response: HTTP/1.1 200 OK content-type: text/html;charset=UTF-8 content-length: 0 connection: keep-alive 可以看出,无论是请求还是响应,都是keep-alive的,但是请求两次,服务器端日志如下: 七月 06, 2019 9:51:27 下午 io.netty.handler.logging.LoggingHandler channelRead 信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ: [id: 0x26d40041, L:/0:0:0:0:0:0:0:1:8080 - R:/0:0:0:0:0:0:0:1:33130] 七月 06, 2019 9:51:27 下午 io.netty.handler.logging.LoggingHandler channelReadComplete 信息: [id: 0xade39344, L:/0:0:0:0:0:0:0:0:8080] READ COMPLETE keepAlive=true channel id=26d40041 http uri: /a.txt?name=chen&f=123;key=456 name=chen f=123 key=456 七月 06, 2019 9:51:29 下午 io....

Filebeat7自定义index的一个坑

Published at June 26, 2019 ·  2 min read

我用的filebeat7来收集日志发给Elastic search,版本是7.1.1,对应的elasticsearch版本和其相同。 默认的,filebeat生成的索引名字是filebeat-7.1.1-2019.06.24这种,不利于区分不同的业务,需要自定义索引,看了下官方文档, 是这么写的 indexedit The index name to write events to. The default is "filebeat-%{[agent.version]}-%{+yyyy.MM.dd}" (for example, "filebeat-7.2.0-2019-06-26"). If you change this setting, you also need to configure the setup.template.name and setup.template.pattern options (see Load the Elasticsearch index template). If you are using the pre-built Kibana dashboards, you also need to set the setup.dashboards.index option (see Load the Kibana dashboards). You can set the index dynamically by using a format string to access any event field....

从钉钉一个忽略了近亿人的产品细节谈谈产品思维

Published at May 6, 2019 ·  1 min read

本来我一个软件工程师,是很不屑提产品的,但时不时总见到一些产品人吹牛皮,也忍不住凑个热闹。 钉钉(DingTalk)是一款由阿里巴巴集团开发的用于商务沟通和工作协同的IM,其和企业版微信占据了中国的大部分企业IM市场。阿里并不是一个擅长做社交的公司,钉钉也是一款命途多舛的产品。2014年左右阿里在内部强行推广来往,一款承载了阿里社交梦的产品,花了巨额的研发和营销费用后,依然是折戟沉沙。后来,来往的团队保留了部分下来,做起了钉钉,针对办公社交,居然做成功了。 在办公社交上,钉钉的崛起甚至早于以社交闻名的腾讯,在社交上扳回了一局,甚至可以说是唯一的一局。微信和QQ在钉钉后也快速推出了办公社交QQ和企业微信等功能,但是在它们推出后,钉钉在很多城市使用的频率已经很高了。 我用的是linux操作系统,钉钉并没有官方linux版本,所以有时候我会使用手机钉钉和网页版钉钉凑合。但是某一次打开钉钉设置的时候,发现了一个问题。 不知大家注意到没有,网页版钉钉的设置使用了流行的switch开关,但是用了红色和绿色的搭配。可能钉钉的设计和开发者觉得“红灯停,绿灯行”的概念已经深入人心,但是他们是否想到了一个事实:中国存在近亿的色盲和色弱用户,这其中又以红绿色盲色弱最多! 红色和绿色,是两个对比度比较接近的颜色,也是最难辨识的两个颜色,别说对色盲和色弱用户来说,即使是对于普通人来说,在某些光线条件下,红绿色也是很难于辨识的。实际上在打开这个页面的时候,我也愣了一会,才辨清了这两个颜色状态。 正因为红绿色是很难于辨识的两种颜色,现在城市的红绿灯,大部分都是掺了蓝色的,所以大家看到的绿灯,都是泛蓝的,而不是单纯的绿色。还有的城市,绿灯不仅掺了蓝色,还会使用动画或声音提示行人车辆,目的就是为了减少了特殊用户甚至是正常用户的困扰。 阿里一直宣传产品的人文关怀,比如雇佣残疾人客服,支付宝支持语音支付等,但是可曾想到,他们另外一款最流行的企业IM软件,却忽略了上亿人! 产品的设计开发中,有许多细节,只有真正用心的人才会注意到,并设计出用户友好的软件,减少用户的困扰。 作者使用了钉钉很久了,最早的APP版设置页面就是使用红色和绿色来作为swith开关的,作者曾经在微博等多个渠道向阿里反馈,可惜一直没有收到阿里的回复,直到一年后的某天,钉钉悄悄地改了这个细节,也不知道是无意中修改还是真的收到了用户的反馈。遗憾的是,网页版钉钉至今没有修改这个细节。...

使用Java自动登录需要动态密码的堡垒机

Published at November 16, 2018 ·  1 min read

公司的生产服务器买了QiZhi Technologie的堡垒机,每次登录都得输入密码+空格+OTOP验证码,都得打开手机APP操作一把,烦不胜烦。 不可忍,想了想,还是借助Java在每次调用时自动生成验证码,然后搞个ssh自动登录(别问我问啥不用公钥,哪有权限啊)得了。 结合之前写的博客 TOTP算法Java版本,很容易就写出计算验证码的代码: public long getCode(String secret, long timeMsec) throws Exception { Base32 codec = new Base32(); byte[] decodedKey = codec.decode(secret); long t = (timeMsec / 1000L) / 30L; for (int i = -window_size; i <= window_size; ++i) { long hash; try { hash = verify_code(decodedKey, t + i); return hash; } catch (Exception e) { e.printStackTrace(); } } return 0L; } 写一个类,专门调用这个方法生成验证码,获取程序执行结果 java -Dfile.encoding=UTF-8 -classpath /soft/tool/authcode/ GoogleAuthTest ,接下来,要实现自动登录就简单多了,先写一个shell...

Linux 下获取文件创建时间

Published at October 7, 2018 ·  2 min read

在Linux里,用户层面并没有文件创建时间的概念,无论是用ls还是stat 指令,都无法获取到文件的创建时间 [tudou@tudou-pc statx]$ stat test-statx.c 文件:test-statx.c 大小:6656 块:16 IO 块:4096 普通文件 设备:805h/2053d Inode:6684737 硬链接:1 权限:(0644/-rw-r--r--) Uid:( 1000/ tudou) Gid:( 1001/ tudou) 最近访问:2018-10-07 13:16:29.000000000 +0800 最近更改:2018-10-07 13:21:09.855461986 +0800 最近改动:2018-10-07 13:21:09.855461986 +0800 创建时间:- 可以看到「创建时间」一行总是「-」。 如果我们使用百度的话,会看到很多文章说,最近改动时间就是创建时间。的确,我们拿很多文件试验了下,这个最近改动时间(Change Time)确实和创建时间很相近,然而Change time并不是Create time,它实际是文件属性修改时间。 试一下即知: [tudou@tudou-pc 下载]$ ./statx ~/.face statx(/home/tudou/.face) = 0 results=fff Size: 7589 Blocks: 16 IO Block: 4096 regular file Device: 08:05 Inode: 5505043 Links: 1 Access: (0644/-rw-r--r--) Uid: 1000 Gid: 1001 Access: 2018-09-16 01:15:52....

linux下解压bin文件

Published at June 2, 2018 ·  1 min read

现在的一些Linux软件很流行使用bin这种安装包格式,只需要下载个安装包就能自动安装解压,比tar.gz省事,比.deb,.rpm的安装包兼容性强,适应范围广。但也有一个问题,bin安装包让人无法知道里面的细节,还是有所顾虑的。比如我前几天需要下载一个JRE6,但Oracle官方在JDK7之前都没有tar.gz包,只有bin包。我肯定不能直接安装bin文件啊,这会破坏我本机已有的JDK8开发环境。 怎么从bin文件里提取出原始安装包呢?其实很简单。用vi打开一个bin文件就知道了,bin文件其实就是一个sh文件和二进制文件的合并文件,前面一段是sh命令,负责实际的安装,它会提取后半部分的二进制数据,后半部分一般是个压缩文件包或者自解压文件的二进制流。 vi jre-for-linux.bin 可以看到,第一行是 #!/bin/bash 接下来就是一堆安装和设置环境变量,提取解压部分了,最关键的部分在这几行 outname=install.sfx.$$ tail ${tail_args} +162 "$0">$outname chmod +x $outname 继续往下看,267行是exit 0,从第268行开始,就是一堆看似乱码的二进制了,到这里那就清晰多了 # 从268行起提取二进制文件 tail -n +268 jre-for-linux.bin >install.sfx # 因为是sfx格式,就用7z解压 7z x install.sfx 到此解压成功。手动安装,使用export设置临时变量,就用上了JRE6了。...

使用内嵌undertow开发调试jfinal项目

Published at May 19, 2018 ·  1 min read

今天在修一个老项目,使用的是jfinal框架,这个框架算是一个比较传统的框架,只支持打包成war运行放入容器中运行,但是在开发过程中可以使用jetty快速启动和调试。个人不是很喜欢jetty,遂换成了undertow。 引入如下依赖 <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-core</artifactId> <version>2.0.1.Final</version> <scope>provided</scope> </dependency> <dependency> <groupId>io.undertow</groupId> <artifactId>undertow-servlet</artifactId> <version>2.0.1.Final</version> <scope>provided</scope> </dependency> 再写一个启动类就好了 public class Main { public static void main(String[] args) throws Exception { DeploymentInfo servletBuilder = Servlets.deployment() .setContextPath("/") .setClassLoader(Main.class.getClassLoader()) .setDeploymentName("zooadmin.war") ; FilterInfo jfinalFilter=new FilterInfo("jfinal",JFinalFilter.class); jfinalFilter.addInitParam("configClass","com.baicai.core.Config"); servletBuilder.addFilter(jfinalFilter); servletBuilder.addFilterUrlMapping("jfinal","/*", DispatcherType.REQUEST); servletBuilder.addFilterUrlMapping("jfinal","/*", DispatcherType.FORWARD); servletBuilder.setResourceManager(new FileResourceManager(new File("src/main/webapp"), 1024)); DeploymentManager manager = Servlets.defaultContainer().addDeployment(servletBuilder); manager.deploy(); PathHandler path = Handlers.path(Handlers.redirect("/")) .addPrefixPath("/", manager.start()); Undertow server = Undertow.builder() .addHttpListener(1080, "localhost") .setHandler(path) .build(); // start server server....

导出freeOTP中的配置

Published at May 14, 2018 ·  1 min read

之前公司的一个网站使用了OTP来做二次验证,然后我就在手机上安装了freeotp这款软件来管理OTP密码,等到换手机了,才发现没法导出原手机的配置,这就尴尬了。FreeOTP is sponsored and officially published by Red Hat,也算是大家闺秀出品的软件,居然不支持这么重要的功能。 试了很多方法,在手机的文件管理器中到处搜索,都没有找到这个配置,基本可以确定freeotp把密钥存放在了系统目录,没有root的话,是没法查看和处理系统目录下的文件,即使用备份工具也备份不出来。 当初网站的OTP二维码也找不到了,网站也没找到重新设置OTP的入口,本着万事不求人的想法,暂时还不想最后求助运维。看来唯一的办法就是root手机了,试了很多工具,没想到kingroot居然支持root魅蓝手机了。 root成功后,马上去freeotp的配置存储目录找到配置文件,找到 /data/data/org.fedorahosted.freeotp/shared_prefs/tokens.xml 文件,得到如下的配置,配置中的引号被转义了 <?xml version='1.0' encoding='utf-8' standalone='yes' ?> <map> <string name="bbcx@qq.com:chen">{"algo":"SHA256","counter":0,"digits":6,"issuerExt":"bbcx@qq.com","label":"chen","period":30,"secret":[17,-56,-42,-70,-48,-79,53],"type":"TOTP"}</string> <string name="bbc">{"algo":"SHA1","counter":0,"digits":6,"issuerExt":"","label":"bbc","period":30,"secret":[0,1,2,3],"type":"TOTP"}</string> <string name="tokenOrder">["bbcx@qq.com:bbcx","bbc"]</string> </map> 可以看出,这里面是就是关于otp的全部配置了,最关键的就是secret字段,这里做了加密,反复试验了半天,没找到解决方案,最终想到Google,找到了这个解决方案: https://github.com/viljoviitanen/freeotp-export/blob/master/README.md ,只需要把tokens.xml贴到这里,https://rawgit.com/viljoviitanen/freeotp-export/master/export-xml.html,就能还原出二维码来,用新手机扫描就好了。 事情还没完,最后想去freeotp的官方那里反应下,没想到官方的态度让我大跌眼镜,https://github.com/freeotp/freeotp-android/issues/20,“出门右转买收费软件去,老子就是不增加备份功能,你能咋地”。 "'''Can I create backupcodes'''? ''No, but if you're using an Android smartphone you can replace the Google Authenticator app with Authenticator Plus. It's a really nice app that can import your existing settings, sync between devices and backup/restore using your sd-card....

Java里常见的几个语法小坑

Published at May 5, 2018 ·  2 min read

很久没更新博客了,想到几个小坑,虽然没啥技术含量,但或许有人不知道呢。 1.删除sublist的元素导致原对象元素被删除 看下面这段代码 List<Integer> students=new ArrayList<Integer>(); for (int i = 0; i <5 ; i++) { students.add(i); } List<Integer> subList=new ArrayList<Integer>(); subList=students.subList(0,5); subList.remove(0); subList.remove(1); for (int i = 0; i <5 ; i++) { System.out.println(i+"="+students.get(i)); } students是个list,然后我们新建立了一个subList对象,这个对象截取了students的一部分,我们删除了subList对象里的一些元素,看下运行结果。 0=1 1=3 2=4 Exception in thread "main" java.lang.IndexOutOfBoundsException: Index: 3, Size: 3 at java.util.ArrayList.rangeCheck(ArrayList.java:657) at java.util.ArrayList.get(ArrayList.java:433) at bai.ListDo.main(ListDo.java:17) 难道说,删除subList对象里的元素也会导致students里的元素被删除?我明明是新建了一个对象啊。然而,事实确实是这样的。 我们要理解一个事情,使用new新建一个对象,只是开辟了一块空间,用来存放这个对象的地址指针,但是这个新建的对象地址,指向的却是原有对象,也就是说,使用subList这个方法的时候,并没有从students里把内容拷贝了一份,仅仅是纪录了一个指针的移动,这样从某种角度来说,是提高了性能节省内存的做法。 看一下subList这个方法的JavaDoc我们就更清楚了。 Returns a view of the portion of this list between the specified * <tt>fromIndex</tt>, inclusive, and <tt>toIndex</tt>, exclusive....