巧用CAS解决数据一致性问题[转载]

这周不太忙的时候看了58到家沈剑老师的一系列的文章,感觉沈剑老师的文章做到了深入浅出,浅显易懂,看完收获很大,有些文章完美的解决了我一直一来的疑惑,所以转载到自己博客,希望对大家也有所帮助。 原文出处微信公众号:架构师之路,微信号:road5858,链接地址:http://mp.weixin.qq.com/s/_XlzbmBSj_i-S2PkE5tI_w 以下是原文: 缘起:在高并发的分布式环境下,对于数据的查询与修改容易引发一致性问题,本文将分享一种非常简单但有效的优化方法。 一、业务场景 业务场景为,购买商品的过程要对余额进行查询与修改,大致的业务流程如下: (1)从数据库查询用户现有余额 SELECT money FROM t_yue WHERE uid=$uid,不妨设查询出来的$old_money=100元 (2)业务层实施业务逻辑,比如购买一个80元的商品,并且打九折 if($old_money> 80*0.9) $new_money=$old_money-80*0.9=28 (3)将数据库中的余额进行修改 UPDAtE t_yue SET money=$new_money WHERE uid=$uid 在并发量低的情况下,这个流程没有任何问题,原有金额100元,购买了80元的九折商品(72元),剩余28元。 二、潜在的问题 在分布式环境中,如果并发量很大,这种“查询+修改”的业务很容易出现数据不一致。极限情况下,可能出现这样的异常流程: (1)业务1和业务2同时查询余额,是100元 (2)业务1和业务2进行逻辑计算,算出各自业务的余额,假设业务1算出的余额是28元,业务2算出的余额是38元 (3)业务1对数据库中的余额先进行修改,设置成28元。 业务2对数据库中的余额后进行修改,设置成38元。 此时异常出现了,原有金额100元,业务1扣除了72元,业务2扣除了62元,最后剩余38元。 三、问题原因 高并发环境下,对同一个数据的并发读(两边都读出余额是100)与并发写(一个写回28,一个写回38)导致的数据一致性问题。 四、原因分析 业务1的写回:原有金额100,这是一个初始状态,写回金额28,理论上只有在原有金额为100的时候才允许写回成功,这一步没问题。 业务2的写回:的原有金额100,这是一个初始状态,写回金额38,理论上只有在原有金额为100的时候才允许写回成功,可实际上,这个时候数据库中的金额已经变为28了,这一步的写操作不应该成功。 五、简易解决方案 在set写回的时候,加上初始状态的条件compare,只有初始状态不变时,才允许set写回成功,这正是大家常说的“Compare And Set”(CAS),是一种常见的降低读写锁冲突,保证数据一致性的方法。 六、业务的升级 业务线使用CAS解决高并发时数据一致性问题,只需要在进行set操作时,compare一下初始值,如果初始值变换,不允许set成功。 对于上文中的业务场景,只需要将“UPDAtEt_yue SET money=$new_money WHERE uid=$uid”升级为 “UPDAtE t_yue SETmoney=$new_money WHERE uid=$uid AND money=$old_money”即可。 并发操作发生时: 业务1执行 => UPDAtE t_yue SET money=28 WHERE uid=$uid AND money=100 业务2执行 => UPDAtE t_yue SET money=38 WHERE uid=$uid AND money=100 【这两个操作同时进行时,只能有一个执行成功】。 ...

July 22, 2017 · 1 min · 89 words · Bridge Li

程序猿的自我修养之开发规范

有感于公司代码比较乱,完全没有规范,而我则受益于实习的时候的老大zeak的严格要求,看到这种情况表示有点难以接受,所以和老大讨论后,基于阿里的规范经过删减写了这么一个标准,今天发出来,不仅供自己时时对照,也供大家参考,最后感谢一下阿里出这份标准。 一、 编程规约 (一)命令风格 整体要求,见名知义,英文为主,类名一般是名称或者形容词,方法名为动词 具体要求: 代码中的命名均不能以下划线或美元符号开始,也不能以下划线或美元符号结束。除非特殊情况最好不要用美元符号(大家猜猜为啥?) 类名使用 UpperCamelCase 风格,必须遵从驼峰形式,但以下情形例外:BO / DTO / VO 方法名、参数名、成员变量、局部变量都统一使用 lowerCamelCase 风格,必须遵从 驼峰形式 常量命名全部大写,单词间用下划线隔开,力求语义表达完整清楚,不要嫌名字长 抽象类命名使用 Abstract 或 Base 开头;异常类命名使用 Exception 结尾;测试类 命名以它要测试的类的名称开始,以 Test 结尾,枚举类名建议带上 Enum 后缀,枚举成员名称需要全大写,单词间用下划线隔开 中括号是数组类型的一部分,数组定义如下:String[] args POJO 类中布尔类型的变量,都不要加 is 包名统一使用小写,点分隔符之间有且仅有一个自然语义的英语单词。包名统一使用 单数形式,但是类名如果有复数含义,类名可以使用复数形式 杜绝完全不规范的缩写,避免望文不知义 如果使用到了设计模式,建议在类名中体现出具体模式 接口类中的方法和属性不要加任何修饰符号(public 也不要加),并加上有效的 Javadoc 注释。尽量不要在接口里定义变量,如果一定要定义变量,肯定是 与接口方法相关,并且是整个应用的基础常量 各层命名规约: 获取单个对象的方法用get或者find做前缀 获取多个对象的方法用list或者query做前缀。 获取统计值的方法用count做前缀。 插入的方法用save(推荐)或insert做前缀。 删除的方法用remove(推荐)或delete做前缀。 修改的方法用update做前缀。 (二) 常量定义 不允许任何魔法值直接出现在代码中 long 或者 Long 初始赋值时,必须使用大写的 L,不能是小写的 l 如果变量值仅在一个范围内变化,且带有名称之外的延伸属性,定义为枚举类 常量类大写,各个单词之前下划线分开 (三) 代码格式 大括号的使用约定: 左大括号前不换行。 左大括号后换行。 右大括号前换行。 右大括号后还有else等代码则不换行;表示终止的右大括号后必须换行。 左小括号和字符之间不出现空格;同样,右小括号和字符之间也不出现空格 if/for/while/switch/do 等保留字与括号之间都必须加空格 任何二目、三目运算符的左右两边都需要加一个空格 缩进采用 4 个空格,禁止使用 tab 字符(根据今年stackoverflow公布数据,空格党的平均工资略高于tab党,大家猜猜为啥?) 单行字符数限制不超过 120 个,超出需要换行,换行时遵循如下原则: 第二行相对第一行缩进 4 个空格,从第三行开始,不再继续缩进。 运算符与下文一起换行。 方法调用的点符号与下文一起换行。 在多个参数超长,在逗号后换行。 在括号前不要换行。 方法参数在定义和传入时,多个参数逗号后边必须加空格 没有必要增加若干空格来使某一行的字符与上一行对应位置的字符对齐 方法体内的执行语句组、变量的定义语句组、不同的业务逻辑之间或者不同的语义之间插入一个空行。相同业务逻辑和语义之间不需要插入空行,没有必要插入多个空行进行隔开 IDE的text file encoding设置为UTF-8; IDE中文件的换行符使用Unix格式, 不要使用 windows 格式。 关于代码格式的设置,eclipse用户可以参考我的GitHub:eclipse设置,按照这个设置,当你保存的时候eclipse会自动帮你formatter你改动过的代码。 ...

July 9, 2017 · 2 min · 286 words · Bridge Li

ThreadLocal类之简单应用示例

在日常开发的系统中,日期处理是非常非常用的一个功能,处理的日期的时候就需要用到SimpleDateFormat对象,但是我们都知道SimpleDateFormat本身不是线程安全的(如果不知道的请看源码),所以就需要频繁创建SimpleDateFormat这个对象。但是我们知道创建这个对象本身不仅是很费时的,而且创建的这些对象存活期很短,导致内存中大量这样的对象需要被GC,所以我们自然而然的想到使用ThreadLocal来给每个线程缓存一个SimpleDateFormat实例,提高性能。下面是一个具体的实现的小例子,其实不仅针对SimpleDateFormat对象,对于数据库连接等等都可以这么使用。 package cn.bridgeli.demo; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.HashMap; import java.util.Map; public class DateFormatFactory { private static final Map<DatePatternEnum, ThreadLocal<DateFormat>> pattern2ThreadLocal; static { DatePatternEnum[] patterns = DatePatternEnum.values(); int len = patterns.length; pattern2ThreadLocal = new HashMap<DatePatternEnum, ThreadLocal<DateFormat>>(len); for (int i = 0; i < len; i++) { DatePatternEnum datePatternEnum = patterns[i]; final String pattern = datePatternEnum.pattern; pattern2ThreadLocal.put(datePatternEnum, new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat(pattern); } }); } } // 获取DateFormat public static DateFormat getDateFormat(DatePatternEnum patternEnum) { ThreadLocal<DateFormat> threadDateFormat = pattern2ThreadLocal.get(patternEnum); // 不需要判断threadDateFormat是否为空 return threadDateFormat.get(); } } 对应的时间枚举类(如果还有其他格式的时间需要处理,可以直接在这个类里面添加即可): ...

June 18, 2017 · 1 min · 114 words · Bridge Li

ThreadLocal类之简单理解

当年实习的时候,当时公司一个相当有经验的工程师zeak带我们,从他那第一次听说了ThreadLocal类,但由于自己基础薄弱,没有理解到底怎么回事,工作中也没有用过,就一直没有太放在心上,刚好这一段时间不太忙,仔细玩了一下,欢迎高手批评。 ThreadLocal,线程本地变量。他为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。简单理解就是,对于非线程安全的变量在线程内部共享不用每次都new,是一种空间换时间的做法。ThreadLocal类提供的几个方法: public T get() { } public void set(T value) { } public void remove() { } protected T initialValue() { } 看名字就知道这些方法是干嘛的了,下面我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。首先是get方法的实现: public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) return (T)e.value; } return setInitialValue(); } 先取得当前线程,然后通过getMap(t)方法获取到一个map,map的类型为ThreadLocalMap。然后接着下面获取到<key,value>键值对,注意这里获取键值对传进去的是 this,而不是当前线程t。如果获取成功,则返回value值。如果map为空,则调用setInitialValue方法返回value。那么getMap方法中又做了什么呢? ThreadLocalMap getMap(Thread t) { return t.threadLocals; } 原来是返回当前线程t中的一个成员变量threadLocals,而threadLocals则是: ThreadLocal.ThreadLocalMap threadLocals = null; 那就看看ThreadLocalMap的实现了: ...

June 11, 2017 · 2 min · 222 words · Bridge Li

Java集合类ArrayList删除特定元素

前一段时间入职新公司,熟悉公司系统原有代码的时候,发现公司代码那个烂啊,系统能正常跑,都不能用侥幸来形容,就是创造了一个奇迹。因为里面不仅没有coding style,而且竟然有很明显的常识性错误。其中当我一眼指出最明显的早就应该出过问题的一个地方,项目组几乎所有成员,是的,几乎全部成员,都说这个还真不知道,涨知识了,那就是:Java集合类ArrayList删除特定元素。发现原来不是所有人都知道怎么做,这难道不是最基础的吗?唉,真不知道这些系统是怎么跑起来的。我们首先看两种错误的写法,第一种: @Test public void testRemove1() { List<String> list = new ArrayList<String>() { private static final long serialVersionUID = 1L; { add("cn"); add("bridgeli"); add("blog"); } }; for (int i = 0, len = list.size(); i < len; i++) { String str = list.get(i); if ("cn".equals(str)) { list.remove(str); } } } 这个写法如果你不知道错在哪,那你得真的好好补基础了。由于这个错误比较明显,所以有人搞了下面这种写法,也是我们公司的同事犯的一个错误: @Test public void testRemove2() { List<String> list = new ArrayList<String>() { private static final long serialVersionUID = 1L; { add("cn"); add("bridgeli"); add("blog"); } }; for (String str : list) { if ("cn".equals(str)) { list.remove(str); } } } 跑一下这个例子看看,把cn换成bridgeli试试,出乎不出乎你的意料?下面我们就来简单探究一下原因foreach的原理,其实特别简单: ...

May 28, 2017 · 1 min · 191 words · Bridge Li

关于synchronized用法的简单理解

synchronized 关键字既可以用于声明方法,也可以用于声明代码块,他们之间有什么区别呢?下面让我们逐一测试一下。 先看以第一个例子: package demo; public class SynchronizedDemo1 { public synchronized static void foo1() { } public synchronized static void foo2() { } } 在这个例子中,foo1 和 foo2 是类的两个静态方法。在不同的线程中,这两个方法的调用时互斥的,不仅是他们之间,任何两个不同的线程的调用也互斥。下面看第二个例子: package demo; public class SynchronizedDemo2 { public synchronized void foo3() { } public synchronized void foo4() { } } 在这个例子中,foo3 和 foo4 是类的两个成员方法,在多线程环境中,调用同一个对象的 foo3 或者 foo4 是互斥的,与上一个例子的差别在于,这是针对同一个对象的多线程方法调用互斥。下面再看最后一个例子: package demo; public class SynchronizedDemo3 { public void foo5() { synchronized (this) { } } public void foo6() { synchronized (SynchronizedDemo3.class) { } } } 在这个例子中,synchronized 用来修饰代码块,需要注意的是:synchronized 后面会有一个参数,其实这个就是用于同步的锁所属的对象。在这个例子中 synchronized (this) 与 SynchronizedDemo2 中加 synchronized 的成员方法是互斥的,而 synchronized (SynchronizedDemo3.class) 与 SynchronizedDemo1 中加 synchronized 的静态方法是互斥的。synchronized 用于修饰代码块会更加灵活,因为除了前面的这个例子外,synchronized 后面的参数可以是任意对象。

May 14, 2017 · 1 min · 99 words · Bridge Li

事务并发处理

前几天和同事讨论,老夫自以为对事务有了一定的了解,但当讨论的时候发现还是有些说不明白,所以周末的时间,又看了一遍带我入门北京尚学堂马士兵老师关于事务的讲解,这次做一下笔记,以供以后忘了的时候查询方便。这里默认读者对事务的ACID都有了了解,直接说事务并发时可能出现的问题和数据库的事务隔离级别 事务并发时可能出现的问题 说这个问题记得大学课堂上有一个很经典的例子就是:银行的存取款,这里也用这个例子说明(因为不知道wp博客怎么搞表格和怎么支持MD,所以就搞几张图片吧) ①. 第一类丢失更新(Lost Update) ②. dirty read脏读(读到了另一个事务在处理中还未提交的数据) ③. non-repeatable read 不可重复读 ④. second lost update problem 第二类丢失更新(不可重复读的特殊情况) ⑤. phantom read 幻读 看到这里可能会有读者对不可重复读和幻读有所迷惑,这两者有什么区别吗?不都是受另一个事务的影响,导致前后结果不一致吗?其实仔细看区别还是很明显的:幻读是关于数据库的delete和insert导致前后的数据不一致,而其他的情况都是数据的更新导致前后的数据不一致 数据库的事务隔离机制 其中在文档java.sql.Connection中有详细的说明,除了none(没有事务)之外,还有:1:read-uncommitted 2:read-committed 4:repeatable read 8:serializable(数字代表对应值)四种。 为什么取值要使用 1 2 4 8 而不是 1 2 3 4 1=0000 2=0010 4=0100 8=1000(位移计算效率高) 需要说明的是: 只要数据库支持事务,就不可能出现第一类丢失更新 read-uncommitted(允许读取未提交的数据) 会出现dirty read, phantom-read, non-repeatable read 问题 read-commited(读取已提交的数据 项目中一般都使用这个)不会出现dirty read,因为只有另一个事务提交才会读出来结果,但仍然会出现 non-repeatable read 和 phantom-read;使用read-commited机制可用悲观锁 乐观锁来解决non-repeatable read 和 phantom-read问题 repeatable read(事务执行中其他事务无法执行修改或插入操作 较安全)但仍然会出现phantom-read serializable解决一切问题(顺序执行事务 不并发,实际中很少用) ...

April 9, 2017 · 1 min · 74 words · Bridge Li

Java GC之常见监控可视化工具总结(下)

上一篇文章总结一下监控和分析的常见命令,那些是基础,但是有些同学看到命令行就害怕,所以这篇文件总计一下两个常用的可视化工具。 JConsole JConsole工具在JDK/bin目录下,启动JConsole后,将自动搜索本机运行的jvm进程,不需要jps命令来查询指定。双击其中一个jvm进程即可开始监控,也可使用“远程进程”来连接远程服务器 进入JConsole主界面,有“概述”、“内存”、“线程”、“类”、“VM摘要”和”Mbean”六个页签: 内存页签相当于jstat命令,用于监视收集器管理的虚拟机内存(Java堆和永久代)变化趋势,还可在详细信息栏观察全部GC执行的时间及次数 线程页签:线程长时间停顿的主要原因有:等待外部资源(数据库连接、网络资源、设备资源等)、死循环、锁等待(活锁和死锁) 最后一个常用页签,VM页签,可清楚的了解显示指定的JVM参数及堆信息 VisualVM:多合一故障处理工具 VisualVM是一个集成多个JDK命令行工具的可视化工具。VisualVM基于NetBeans平台开发,它具备了插件扩展功能的特性,通过插件的扩展,可用于显示虚拟机进程及进程的配置和环境信息(jps,jinfo),监视应用程序的CPU、GC、堆、方法区及线程的信息(jstat、jstack)等。VisualVM在JDK/bin目录下 ①. 安装插件: 工具- 插件 ②. 监控垃圾回收 在左侧的“Application”测看下,有个“Local”节点,所有本地正在运行的Java应用都将罗列在这里。Java VisualVM是一个Java应用。所以,它将自己也列在这里。为了方便学习,我们将监控Java VisualVM自身的垃圾回收过程。双击“Local”节点下的VisualVM图标,现在,应用监视窗口在右侧打开。我们关注的是“Visual GC”,点击它 再配合其他的标签页,例如“Threads”以及线程转储你,我们就可以深入详细地了解这方面的内容。在“监视”标签页,我们可以监控整个堆内存的使用情况,这些都不贴图了,大家可以随便玩。 ③. 在VisualVM中生成dump文件 参考资料:周志明《深入理解Java虚拟机》第二版第四章

April 4, 2017 · 1 min · 23 words · Bridge Li

Java GC之常见监控与分析命令总结(上)

上一篇文章简单写了几种常见的垃圾收集器的参数设置,设置参数的时候离不开对对系统进行监控和分析,所以总结一下监控和分析的常见命令。 jps:JVM Process Status Tool,显示指定系统内所有的HotSpot虚拟机进程 命令格式: jps \[options\] \[hostid\] hostid为RMI注册表中注册的主机名,其他常用参数如下: -q:只输出LVMID,省略主类的名称 -m:输出虚拟机进程启动的时候传递给主类main()方法的参数 -l:输出主类的全名,如果进程执行的是jar包,输出jar路径 -v:输出虚拟机进程启动时JVM参数 命令执行样例: jps -l jstat:JVM Statistics Monitoring Tool,用于收集Hotspot虚拟机各方面的运行数据 命令格式: jstat \[option vmid [interval[s|ms\] \[count\]]] 对于命令格式中的VMID和LVMID,如过是本地虚拟机进程,VMID和LVMID是一致的,如果是远程虚拟机,那VMID的格式应当是: \[protocol:\] \[//\] lvmid[@hostname[:port]/servername] interval和count代表查询的间隔和次数,如果省略这两个参数,说明只查一次,其他常用参数: -class:监视装载类、卸载类、总空间以及类装载所耗费的时间 -gc:监视java堆状况,包括eden区、两个survivor区、老年代、永久代等的容量、已用空间、GC时间合计信息 -gccapacity:监视内容与-gc基本相同,但输出主要关注java堆各个区域使用到最大、最小空间 -gcutil:监视内容与-gc基本相同,但输出主要关注已使用控件占总空间的百分比 -gccause:与-gcutil功能一样,但是会额外输出导致上一次gc产生的原因 -gcnew:监视新生代GC情况 -gcnewcapacity:监视内容与-gcnew基本相同,输出主要关注使用到的最大、最小空间 -gcold:监视老年代GC情况 -gcoldcapacity:监视内容与-gcold基本相同,输出主要关注使用到的最大、最小空间 -gcpermcapacity:输出永久代使用到的最大、最小空间 -compiler:输出JIT编译过的方法、耗时等信息 -printcompilation:输出已经被JIT编译过的方法 命令执行样例: jstat -gcutil 2764 1000 jinfo:Configuration Info for Java,显示虚拟机的配置信息 使用jps命令的-v参数可以查看虚拟机启动时显示指定的参数列表,但如果想知道未被显式指定的参数的系统默认值,除了去找资料以外,就得使用jinfo的-flag选项,命令格式: jinfo [option] pid 执行样例:查询CMSInitiatingOccupancyFraction参数值 jinfo -flag CMSInitiatingOccupancyFraction 2764 jmap:Memory Map for Java,生成虚拟机的内存转储快照(heapdump文件) 命令格式: ...

March 19, 2017 · 1 min · 159 words · Bridge Li

Java GC之常见垃圾收集器参数总结

上一篇文章简单写了几种常见的垃圾收集器,俗话说,好记性不如烂笔头,今天总结一下这些垃圾收集器的参数总结,供自己和需要的读者将来查阅 -XX:+UseSerialGC : Jvm运行在Client模式下的默认值,打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收 -XX:+UseParNewGC : 打开此开关后,使用ParNew + Serial Old的收集器进行垃圾回收 -XX:+UseConcMarkSweepGC : 使用ParNew + CMS + Serial Old的收集器组合进行内存回收,Serial Old作为CMS出现“Concurrent Mode Failure”失败后的后备收集器使用 -XX:+UseParallelGC : Jvm运行在Server模式下的默认值,打开此开关后,使用Parallel Scavenge + Serial Old的收集器组合进行回收 -XX:+UseParallelOldGC : 使用Parallel Scavenge + Parallel Old的收集器组合进行回收 -XX:SurvivorRatio : 新生代中Eden区域与Survivor区域的容量比值,默认为8,代表Eden:Subrvivor = 8:1 -XX:PretenureSizeThreshold : 直接晋升到老年代对象的大小,设置这个参数后,大于这个参数的对象将直接在老年代分配 -XX:MaxTenuringThreshold : 晋升到老年代的对象年龄,每次Minor GC之后,年龄就加1,当超过这个参数的值时进入老年代 -XX:UseAdaptiveSizePolicy : 动态调整java堆中各个区域的大小以及进入老年代的年龄 -XX:+HandlePromotionFailure : 是否允许新生代收集担保,进行一次minor gc后, 另一块Survivor空间不足时,将直接会在老年代中保留 -XX:ParallelGCThreads : 设置并行GC进行内存回收的线程数 -XX:GCTimeRatio : GC时间占总时间的比列,默认值为99,即允许1%的GC时间,仅在使用Parallel Scavenge 收集器时有效 -XX:MaxGCPauseMillis : 设置GC的最大停顿时间,在Parallel Scavenge 收集器下有效 ...

March 5, 2017 · 1 min · 96 words · Bridge Li