MySQL中不要将字符串与数字比较

mysql在将字符串与整数比较时存在一个坑:

select * from xxxx
where xx_id =97

%title插图%num

得到下面的数据,可以看到第2列是字符串类型,明显与整数不相等,但是mysql却当做相等查询了出来,mysql在处理字符串与整形数据对比时,会依次将字符串与整形数据对比,直到字符串不为整数的字母为止!

所以我们在使用mysql时一定要注意这一点,使用同类型数据对比!

 

 

spring的Bean初始化方法的2种方式

环境 jdk1.8、springboot、idea

 

代码案例

  • 【方式一】实现接口方式InitializingBean

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 【方式二】通过xml配置方式<bean init-method=”myInit” /bean>

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 运行结果

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

spring源码分析

  • org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactoryinvokeInitMethods方法 【自己可以找下】

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 注意这里是直接通过Bean的方法去调用afterPropertiesSet

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 这里是<bean init-method=”myInit” /bean>执行的地方

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 下面是debug的信息

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • 通过反射java.lang.reflect.Method的invoke方法去执行

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

  • DisposableBean和 destroy-method=”myDestroy” 的源码
  • 也是一个Bean方法直接调用,一个反射destroy-method

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

面试官:说下spring的Bean初始化方法的2种方式 程序员:不知道

 

总结

  • Spring 2种Bean初始方法的执行方式,可以同时执行,接口方式InitializingBean先执行,配置init-method方式后执行,详情请见spring源码
  • 执行效率分析,接口InitializingBean方式比 通过反射配置init-method方式快。
  • 其他特性请debug进去自己看看,源码还是要看看的。

使用 v-cloak 防止页面加载时出现 vuejs 的变量名

使用 vuejs 做了一个简单的功能页面,逻辑是,页面加载后获取当前的经纬度,然后通过 ajax 从后台拉取附近的小区列表。但是 bug 出现了,在显示小区列表之前,会闪现小区名对应的 vuejs 变量名。

案发现场的 HTML 代码

  1. <ul v-for=“item in items”>
  2. <li>{{ item.name }}</li>
  3. </ul>

页面加载时,会闪现

{{ item.name }}

Google 了一下,发现 vuejs 内置的 directive v-cloak 可以解决这个问题。非常简单

HTML 修改成

  1. <ul v-cloak v-for=“item in items”>
  2. <li>{{ item.name }}</li>
  3. </ul>

CSS 中添加

  1. [v-cloak] {
  2. display: none;
  3. }

搞定!

但是原理是什么呢?

这段 CSS 的含义是,包含 v-cloak (cloak n. 披风,斗篷;vt. 遮盖,掩盖) 属性的 html 标签在页面初始化时会被隐藏。

在 vuejs instance ready 之后,v-cloak 属性会被自动去除,也就是对应的标签会变为可见

Redis缓存穿透、缓存雪崩、redis并发问题分析

把redis作为缓存使用已经是司空见惯,但是使用redis后也可能会碰到一系列的问题,尤其是数据量很大的时候,经典的几个问题如下:

(一)缓存和数据库间数据一致性问题

分布式环境下(单机就不用说了)非常容易出现缓存和数据库间的数据一致性问题,针对这一点的话,只能说,如果你的项目对缓存的要求是强一致性的,那么请不要使用缓存。我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括 合适的缓存更新策略,更新数据库后要及时更新缓存、缓存失败时增加重试机制,例如MQ模式的消息队列。

(二)缓存击穿问题

缓存击穿表示恶意用户模拟请求很多缓存中不存在的数据,由于缓存中都没有,导致这些请求短时间内直接落在了数据库上,导致数据库异常。这个我们在实际项目就遇到了,有些抢购活动、秒杀活动的接口API被大量的恶意用户刷,导致短时间内数据库宕机了,好在数据库是多主多从的,hold住了。

解决方案的话:

1、使用互斥锁排队

业界比价普遍的一种做法,即根据key获取value值为空时,锁上,从数据库中load数据后再释放锁。若其它线程获取锁失败,则等待一段时间后重试。这里要注意,分布式环境中要使用分布式锁,单机的话用普通的锁(synchronized、Lock)就够了。

public String getWithLock(String key, Jedis jedis, String lockKey, String uniqueId, long expireTime) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 // 分布式锁,详细可以参考https://blog.csdn.net/fanrenxiang/article/details/79803037
 //封装的tryDistributedLock包括setnx和expire两个功能,在低版本的redis中不支持
 try {
 boolean locked = redisService.tryDistributedLock(jedis, lockKey, uniqueId, expireTime);
 if (locked) {
 value = userService.getById(key);
 redisService.set(key, value);
 redisService.del(lockKey);
 return value;
 } else {
 // 其它线程进来了没获取到锁便等待50ms后重试
 Thread.sleep(50);
 getWithLock(key, jedis, lockKey, uniqueId, expireTime);
 }
 } catch (Exception e) {
 log.error("getWithLock exception=" + e);
 return value;
 } finally {
 redisService.releaseDistributedLock(jedis, lockKey, uniqueId);
 }
 }
 return value;
}

这样做思路比较清晰,也从一定程度上减轻数据库压力,但是锁机制使得逻辑的复杂度增加,吞吐量也降低了,有点治标不治本。

2、布隆过滤器(推荐)

bloomfilter就类似于一个hash set,用于快速判某个元素是否存在于集合中,其典型的应用场景就是快速判断一个key是否存在于某容器,不存在就直接返回。布隆过滤器的关键就在于hash算法和容器大小,下面先来简单的实现下看看效果,我这里用guava实现的布隆过滤器:

<dependencies> 
 <dependency> 
 <groupId>com.google.guava</groupId> 
 <artifactId>guava</artifactId> 
 <version>23.0</version> 
 </dependency> 
</dependencies> 
public class BloomFilterTest {
 
 private static final int capacity = 1000000;
 private static final int key = 999998;
 
 private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity);
 
 static {
 for (int i = 0; i < capacity; i++) {
 bloomFilter.put(i);
 }
 }
 
 public static void main(String[] args) {
 /*返回计算机*精确的时间,单位微妙*/
 long start = System.nanoTime();
 
 if (bloomFilter.mightContain(key)) {
 System.out.println("成功过滤到" + key);
 }
 long end = System.nanoTime();
 System.out.println("布隆过滤器消耗时间:" + (end - start));
 int sum = 0;
 for (int i = capacity + 20000; i < capacity + 30000; i++) {
 if (bloomFilter.mightContain(i)) {
 sum = sum + 1;
 }
 }
 System.out.println("错判率为:" + sum);
 }
}
成功过滤到999998
布隆过滤器消耗时间:215518
错判率为:318

可以看到,100w个数据中只消耗了约0.2毫秒就匹配到了key,速度足够快。然后模拟了1w个不存在于布隆过滤器中的key,匹配错误率为318/10000,也就是说,出错率大概为3%,跟踪下BloomFilter的源码发现默认的容错率就是0.03:

public static <T> BloomFilter<T> create(Funnel<T> funnel, int expectedInsertions /* n */) {
 return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}

我们可调用BloomFilter的这个方法显式的指定误判率:

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);

我们断点跟踪下,误判率为0.02和默认的0.03时候的区别:

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

Redis缓存穿透、缓存雪崩、redis并发问题分析

 

 

对比两个出错率可以发现,误判率为0.02时数组大小为8142363,0.03时为7298440,误判率降低了0.01,BloomFilter维护的数组大小也减少了843923,可见BloomFilter默认的误判率0.03是设计者权衡系统性能后得出的值。要注意的是,布隆过滤器不支持删除操作。用在这边解决缓存穿透问题就是:

public String getByKey(String key) {
 // 通过key获取value
 String value = redisService.get(key);
 if (StringUtil.isEmpty(value)) {
 if (bloomFilter.mightContain(key)) {
 value = userService.getById(key);
 redisService.set(key, value);
 return value;
 } else {
 return null;
 }
 }
 return value;
}

(三)缓存雪崩问题

缓存在同一时间内大量键过期(失效),接着来的一大波请求瞬间都落在了数据库中导致连接异常。

解决方案:

1、也是像解决缓存穿透一样加锁排队,实现同上;

2、建立备份缓存,缓存A和缓存B,A设置超时时间,B不设值超时时间,先从A读缓存,A没有读B,并且更新A缓存和B缓存;

public String getByKey(String keyA,String keyB) {
 String value = redisService.get(keyA);
 if (StringUtil.isEmpty(value)) {
 value = redisService.get(keyB);
 String newValue = getFromDbById();
 redisService.set(keyA,newValue,31, TimeUnit.DAYS);
 redisService.set(keyB,newValue);
 }
 return value;
}

(四)缓存并发问题

这里的并发指的是多个redis的client同时set key引起的并发问题。比较有效的解决方案就是把redis.set操作放在队列中使其串行化,必须的一个一个执行,具体的代码就不上了,当然加锁也是可以的,至于为什么不用redis中的事务,留给各位看官自己思考探究。

FastJSON实现详解

还记得电影《功夫》中火云邪神的一句话:天下功夫,无坚不破,唯快不破。在程序员的世界中,“快”一直是大家苦苦修炼,竞相追逐的终*目标之一,甚至到了“不择手段”、“锱铢必较”的地步。

一直使用json游离于各种编程语言和系统之间。一个偶然的机会碰到了Fastjson,被他的无依赖、易使用、应用广等特性深深吸引的同时,更被他出奇的“快”所震惊,在Java界犹如一骑*尘,旁人只能望其项背。很自然的一个想法涌上心头:FastJSON为何如此之快?于是定神来拔一拔其实现,一则膜拜大师的杰作,二则虚心偷技,三则方便来者学习。

本篇接下来的内容是基于FastJSON 1.1.40,着重讲述其序列化、反序列化实现,*后分析FastJSON为何如此“fast”的原因。

1. 序列化

所谓序列化,就是将java各种对象转化为json串。不多说,先上图。

%title插图%num

 序列化入口

平常我们经常用到的是JSON.toJSONString()这个静态方法来实现序列化。其实JSON是一个抽象类,该类实现了JSONAware(转为json串)和JSONStreamAware(将json串写入Appendable中)的接口,同时又是JSONArray(内部实现就是个List)和JSONObject(内部实现就是个Map)的父类。JSON.toJSONString()方法内部实现基本相同,为做某些特定配置,对外暴露的接口可能不同。该方法的实现实际托付给了JSONSerializer类。

序列化组合器

JSONSerializer类相当于一个序列化组合器,它将上层调用、序列化配置、具体类型序列化实现、序列化字符串拼接等功能组合在一起,方便外部统一调用。该类有几个重要的成员,SerializeConfig、SerializeWriter、各种Filter列表、DateFormat、SerialContext等,还有每次对各个具体对象序列化的ObjectSerializer(非JSONSerializer的成员变量)。下面就来挨个说明其各自功能。

1. SerializeConfig

SerializeConfig是全局唯一的,它继承自IdentityHashMap,IdentityHashMap是一个长度默认为1024的Hash桶,每个桶存放相同Hash的Entry(可看做链表节点,包含key、value、next指针、hash值)做成的单向链表,IdentityHashMap实现了HashMap的功能,但能避免HashMap并发时的死循环。

SerializeConfig的主要功能是配置并记录每种Java类型对应的序列化类(ObjectSerializer接口的实现类),比如Boolean.class使用BooleanCodec(看命名就知道该类将序列化和反序列化实现写到一起了)作为序列化实现类,float[].class使用FloatArraySerializer作为序列化实现类。这些序列化实现类,有的是FastJSON中默认实现的(比如Java基本类),有的是通过ASM框架生成的(比如用户自定义类),有的甚至是用户自定义的序列化类(比如Date类型框架默认实现是转为毫秒,应用需要转为秒)。当然,这就涉及到是使用ASM生成序列化类还是使用JavaBean的序列化类类序列化的问题,这里判断根据就是是否Android环境(环境变量”java.vm.name”为”dalvik”或”lemur”就是Android环境),但判断不仅这里一处,后续还有更具体的判断。

2. SerializeWriter

SerializeWriter继承自Java的Writer,其实就是个转为FastJSON而生的StringBuilder,完成高性能的字符串拼接。该类成员如下:

 

  • char buf[]

可理解为每次序列化后字符串的内存存放地址。

 

  • static ThreadLocal> bufLocal

每次序列化,都需要重新分配buf[]内存空间。而bufLocal就是每次序列化后bug[]的内存空间保留到ThreadLocal里,但其中的值清空,避免频繁的内存分配和gc。

 

  • int features

生成json字符串的特征配置,默认配置为:

<span>QuoteFieldNames | SkipTransientField | WriteEnumUsingToString | SortField</span>

表示含义为:双引号filedName and 忽略transientField and enum类型使用String写入 and 排序输出field。 支持的所有特征在SerializerFeature类中,用户可在调用时显示配置,也可通过JSONFiled或JSONType注入配置。

 

  • Writer

writer 用户指定将生成的json串直接写入某writer中,比如JSONWriter类。

举个例子吧,writeStringWithDoubleQuote()表示用字符串用双引号写入,看看如何拼接字符串的。

3. Filter列表

SerializeWriter中有很多Filter列表,可视为在生成json串的各阶段、各地方定制序列化,大致如下:

 

  • BeforeFilter :序列化时在*前面添加内容
  • AfterFilter :序列化时在*后面添加内容
  • PropertyFilter :根据PropertyName和PropertyValue来判断是否序列化
  • ValueFilter :修改Value
  • NameFilter :修改key
  • PropertyPreFilter :根据PropertyName判断是否序列化

4. DateFormat

指定日期格式。若不指定,FastJSON会自动识别如下日期格式:

 

  • ISO-8601日期格式
  • yyyy-MM-dd
  • yyyy-MM-dd HH:mm:ss
  • yyyy-MM-dd HH:mm:ss.SSS
  • 毫秒数值
  • 毫秒字符串
  • .Net Json日期格式
  • new Date()

5. SerialContext

序列化上下文,在引用或循环引用中使用,该值会放入references的Hash桶(IdentityHashMap)缓存。

6. ObjectSerializer  

ObjectSerializer只有一个接口方法,如下:

void write(JSONSerializer serializer,Objectobject,Object
    fieldName,Type fieldType);

可见,将JSONSerializer传入了ObjectSerializer中,而JSONSerializer有SerializeWriter成员,在每个具体ObjectSerializer实现中,直接使用SerializeWriter拼接字符串即可;Object即是待序列化的对象;fieldName则主要用于组合类引用时设置序列化上下文;而fieldType主要是为了泛型处理。

JSONSerializer中通过public ObjectSerializer getObjectWriter(Class clazz)函数获取类对应的序列化类(即实现ObjectSerializer接口的类),大致逻辑如下:

%title插图%num

整个过程是先获取已实现基础类对应的序列化类,再通过类加载器获取自定义的AutowiredObjectSerializer序列化类,*后获取通过createJavaBeanSerializer()创建的序列化类。通过该方法会获取两种序列化类,一种是直接的JavaBeanSerializer(根据类的get方法、public filed等JavaBean特征序列化),另一种是createASMSerializer(通过ASM框架生成的序列化字节码),优先使用第二种。选择JavaBeanSerializer的条件为:

 

  • 该clazz为非public类
  • 该clazz的类加载器在ASMClassLoader的外部,或者clazz就是 Serializable.class,或者clazz就是Object.class
  • JSONType的注解指明不适用ASM
  • createASMSerializer加载失败

结合前面的讨论,可以得出使用ASM的条件:非Android系统、非基础类、非自定义的AutowiredObjectSerializer、非以上所列的使用JavaBeanSerializer条件。

具体基础类的序列化方法、JavaBeanSerializer的序列化方法和ASM生成的序列化方法可以参见代码,这里就不做一一讲解了。

2. 反序列化

所谓反序列化,就是将json串转化为对应的java对象。还是先上图。

%title插图%num

同样是JSON类作为反序列化入口,实现了parse()、parseObject()、parseArray()等将json串转换为java对象的静态方法。这些方法的实现,实际托付给了DefaultJSONParser类。

DefaultJSONParser类相当于序列化的JSONSerializer类,是个功能组合器,它将上层调用、反序列化配置、反序列化实现、词法解析等功能组合在一起,相当于设计模式中的外观模式,供外部统一调用。同样,我们来分析该类的几个重要成员,看看他是如何实现纷繁的反序列化功能的。

1.  ParserConfig

同SerializeConfig,该类也是全局唯一的解析配置,其中的boolean asmEnable同样判断是否为Andriod环境。与SerializeConfig不同的是,配置类和对应反序列类的IdentityHashMap是该类的私有成员,构造函数的时候就将基础反序列化类加载进入IdentityHashMap中。

2.  JSONLexer 

JSONLexer是个接口类,定义了各种当前状态和操作接口。JSONLexerBase是对JSONLexer实现的抽象类,类似于序列化的SerializeWriter类,专门解析json字符串,并做了很多优化。实际使用的是JSONLexerBase的两个子类JSONScanner和JSONLexerBase,前者是对整个字符串的反序列化,后者是接Reader直接序列化。简析JSONLexerBase的某些成员:

 

  • int token

由于json串具有一定格式,字符串会根据某些特定的字符来自解释所表示的意义,那么这些特定的字符或所处位置的字符在FastJSON中就叫一个token,比如”(“,”{“,”[“,”,”,”:”,key,value等,这些都定义在JSONToken类中。

 

  • char[] sbuf

解析器通过扫描输入字符串,将匹配得到的*细粒度的key、value会放到sbuf中。

 

  • static ThreadLocal> SBUF_REF_LOCAL

上面sbuf的空间不释放,在下次需要的时候直接拿出来使用,从避免的内存的频繁分配和gc。

 

  • features

反序列化特性的配置,同序列化的feature是通过int的位或来实现其特性开启还是关闭的。默认配置是:AutoCloseSource | UseBigDecimal | AllowUnQuotedFieldNames | AllowSingleQuotes |AllowArbitraryCommas | AllowArbitraryCommas | SortFeidFastMatch |IgnoreNotMatch ,表示检查json串的完整性 and 转换数值使用BigDecimal and 允许接受不使用引号的filedName and 允许接受使用单引号的key和value and 允许接受连续多个”,”的json串 and 使用排序后的field做快速匹配 and 忽略不匹配的key/value对。当然,这些参数也是可以通过其他途径配置的。

 

  • hasSpecial

对转义符的处理,比如’\0’,’\’等。

词法解析器是基于预测的算法从左到右一次遍历的。由于json串具有自身的特点,比如为key的token后*有可能是”:”,”:”之后可能是value的token或为”{“的token或为”[“的token等等,从而可以根据前一个token预判下一个token的可能,进而得知每个token的含义。分辨出各个token后,就可以获取具体值了,比如scanString获取key值,scanFieldString根据fieldName获取fieldValue,scanTrue获取java的true等等。其中,一般会对key进行缓存,放入SymbolTable(类似于IdentityHashMap)中,猜想这样做的目的是:应用解析的json串一般key就那么多,每次生成开销太多,干脆缓存着,用的就是就来取,还是空间换时间的技巧。

3.  List< ExtraTypeProvider >和List< ExtraProcessor >

视为对其他类型的处理和其他自定义处理而留的口子,用户可以自己实现对应接口即可。

4.  DateFormat

同序列化的DateFormat,不多说了。

5.  ParseContext 和 List< ResolveTask >

ParseContext同序列化的SerialContext,为引用甚至循环引用做准备。

List< ResolveTask >当然就是处理这种多层次甚至多重引用记录的list了。

6.  SymbolTable

上面提到的key缓存。

7.  ObjectDeserializer

跟ObjectSerializer也是相似的。先根据fieldType获取已缓存的解析器,如果没有则根据fieldClass获取已缓存的解析器,否则根据注解的JSONType获取解析器,否则通过当前线程加载器加载的AutowiredObjectDeserializer查找解析器,否则判断是否为几种常用泛型(比如Collection、Map等),*后通过createJavaBeanDeserializer来创建对应的解析器。当然,这里又分为JavaBeanDeserializer和asmFactory.createJavaBeanDeserializer两种。使用asm的条件如下:

 

  • 非Android系统
  • 该类及其除Object之外的所有父类为是public的
  • 泛型参数非空
  • 非asmFactory加载器之外的加载器加载的类
  • 非接口类
  • 类的setter函数不大于200
  • 类有默认构造函数
  • 类不能含有仅有getter的filed
  • 类不能含有非public的field
  • 类不能含有非静态的成员类
  • 类本身不是非静态的成员类

使用ASM生成的反序列化器具有较高的反序列化性能,比如对排序的json串可按顺序匹配解析,从而减少读取的token数,但如上要求也是蛮严格的。综上,FastJSON反序列化也支持基础反序列化器、JavaBeanDeserializer反序列化器和ASM构造的反序列化器,这里也不做一一讲解了。

3. Why So Fast

FastJSON真的很快,读后受益匪浅。个人总结了下快的原因(不一定完整):

1.  专业的心做专业的事

不论是序列化还是反序列化,FastJSON针对每种类型都有与之对应的序列化和反序列化方法,就针对这种类型来做,优化性能自然更具针对性。自编符合json的SerializeWriter和JSONLexer,就连ASM框架也给简化掉了,只保留所需部分。不得不叹其用心良苦。

2.  无处不在的缓存

空间换时间的想法为程序员屡试不爽,而作者将该方法用到任何细微之处:类对应的序列化器/反序列化器全部存起来,方便取用;解析的key存起来,表面重复内存分配等等。

3.  不厌其烦的重复代码

我不知道是否作者故意为之,程序中出现了很多类似的代码,比如特殊字符处理、不同函数对相同token的处理等。这样虽对于程序员寻求规整相违背,不过二进制代码却很喜欢,无形之中减少了许多函数调用。

4.  不走寻常路

对于JavaBean,可以通过发射实现序列化和反序列化(FastJSON已有实现),但默认使用的是ASM框架生成对应字节码。为了性能,无所不用其*。

5.  一点点改变有很大的差别

排序对输出仅是一点小小的改变,丝毫不影响json的使用,但却被作者用在了解析的快速匹配上,而不用挨个拎出key。

6.  从规律中找性能

上面也讲到,FastJSON读取token基于预测的。json串自身的规律性被作者逮个正着,预测下一个将出现的token处理比迷迷糊糊拿到一个token再分情况处理更快捷。

MySQL的四种事务隔离级别

本文实验的测试环境:Windows 10+cmd+MySQL5.6.36+InnoDB

一、事务的基本要素(ACID)

  1、原子性(Atomicity):事务开始后所有操作,要么全部做完,要么全部不做,不可能停滞在中间环节。事务执行过程中出错,会回滚到事务开始前的状态,所有的操作就像没有发生一样。也就是说事务是一个不可分割的整体,就像化学中学过的原子,是物质构成的基本单位。

   2、一致性(Consistency):事务开始前和结束后,数据库的完整性约束没有被破坏 。比如A向B转账,不可能A扣了钱,B却没收到。

   3、隔离性(Isolation):同一时间,只允许一个事务请求同一数据,不同的事务之间彼此没有任何干扰。比如A正在从一张银行卡中取钱,在A取钱的过程结束前,B不能向这张卡转账。

   4、持久性(Durability):事务完成后,事务对数据库的所有更新将被保存到数据库,不能回滚。

 

二、事务的并发问题

  1、脏读:事务A读取了事务B更新的数据,然后B回滚操作,那么A读取到的数据是脏数据

  2、不可重复读:事务 A 多次读取同一数据,事务 B 在事务A多次读取的过程中,对数据作了更新并提交,导致事务A多次读取同一数据时,结果 不一致。

  3、幻读:系统管理员A将数据库中所有学生的成绩从具体分数改为ABCDE等级,但是系统管理员B就在这个时候插入了一条具体分数的记录,当系统管理员A改结束后发现还有一条记录没有改过来,就好像发生了幻觉一样,这就叫幻读。

  小结:不可重复读的和幻读很容易混淆,不可重复读侧重于修改,幻读侧重于新增或删除。解决不可重复读的问题只需锁住满足条件的行,解决幻读需要锁表

 

三、MySQL事务隔离级别

事务隔离级别 脏读 不可重复读 幻读
读未提交(read-uncommitted)
不可重复读(read-committed)
可重复读(repeatable-read)
串行化(serializable)

 

 

 

 

 

mysql默认的事务隔离级别为repeatable-read

%title插图%num

 

四、用例子说明各个隔离级别的情况

1、读未提交:

(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:

%title插图%num

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

%title插图%num

 

(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:

%title插图%num

(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:

%title插图%num

(5)在客户端A执行更新语句update account set balance = balance – 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别

%title插图%num

 

2、读已提交

(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:

%title插图%num

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:

%title插图%num

(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:

%title插图%num

(4)客户端B的事务提交

%title插图%num

(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题

%title插图%num

 

3、可重复读

(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录

%title插图%num

(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交

%title插图%num

(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题

%title插图%num

(4)在客户端A,接着执行update balance = balance – 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。

%title插图%num

(5)重新打开客户端B,插入一条新数据后提交

%title插图%num

(6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读

%title插图%num

 

4.串行化

(1)打开一个客户端A,并设置当前事务模式为serializable,查询表account的初始值:

复制代码

mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> select * from account;
+------+--------+---------+
| id   | name   | balance |
+------+--------+---------+
|    1 | lilei  |   10000 |
|    2 | hanmei |   10000 |
|    3 | lucy   |   10000 |
|    4 | lily   |   10000 |
+------+--------+---------+
4 rows in set (0.00 sec)

复制代码

(2)打开一个客户端B,并设置当前事务模式为serializable,插入一条记录报错,表被锁了插入失败,mysql中事务隔离级别为serializable时会锁表,因此不会出现幻读的情况,这种隔离级别并发性*低,开发中很少会用到。

复制代码

mysql> set session transaction isolation level serializable;
Query OK, 0 rows affected (0.00 sec)

mysql> start transaction;
Query OK, 0 rows affected (0.00 sec)

mysql> insert into account values(5,'tom',0);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

复制代码

 

补充:

  1、事务隔离级别为读提交时,写数据只会锁住相应的行

  2、事务隔离级别为可重复读时,如果检索条件有索引(包括主键索引)的时候,默认加锁方式是next-key 锁;如果检索条件没有索引,更新数据时会锁住整张表。一个间隙被事务加了锁,其他事务是不能在这个间隙插入记录的,这样可以防止幻读。

  3、事务隔离级别为串行化时,读写数据都会锁住整张表

   4、隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

   5、MYSQL MVCC实现机制参考链接:https://blog.csdn.net/whoamiyang/article/details/51901888

   6、关于next-key 锁可以参考链接:https://blog.csdn.net/bigtree_3721/article/details/73731377

spring注解-@Transactional事务几点注意

这里面有几点需要大家留意:
A. 一个功能是否要事务,必须纳入设计、编码考虑。不能仅仅完成了基本功能就ok。
B. 如果加了事务,必须做好开发环境测试(测试环境也尽量触发异常、测试回滚),确保事务生效。
C. 以下列了事务使用过程的注意事项,请大家留意。
1. 不要在接口上声明@Transactional ,而要在具体类的方法上使用 @Transactional 注解,否则注解可能无效。
2.不要图省事,将@Transactional放置在类级的声明中,放在类声明,会使得所有方法都有事务。故@Transactional应该放在方法级别,不需要使用事务的方法,就不要放置事务,比如查询方法。否则对性能是有影响的。
3.使用了@Transactional的方法,对同一个类里面的方法调用, @Transactional无效。比如有一个类Test,它的一个方法A,A再调用Test本类的方法B(不管B是否public还是private),但A没有声明注解事务,而B有。则外部调用A之后,B的事务是不会起作用的。(经常在这里出错)
4.使用了@Transactional的方法,只能是public,@Transactional注解的方法都是被外部其他类调用才有效,故只能是public。道理和上面的有关联。故在 protected、private 或者 package-visible 的方法上使用 @Transactional 注解,它也不会报错,但事务无效。
5.经过在ICORE-CLAIM中测试,效果如下:
A.抛出受查异常XXXException,事务会回滚。
B.抛出运行时异常NullPointerException,事务会回滚。
C.Quartz中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)
D.异步任务中,execute直接调用加了@Transactional方法,可以回滚;间接调用,不会回滚。(即上文3点提到的)
E.在action中加上@Transactional,不会回滚。切记不要在action中加上事务。
F.在service中加上@Transactional,如果是action直接调该方法,会回滚,如果是间接调,不会回滚。(即上文3提到的)
G.在service中的private加上@Transactional,事务不会回滚。

spring事务@Transactional在同一个类中的方法调用不生效

spring提供了非常强大的事务管理机制,之前一直以为只要在想要加注解的方法上加个@Transactional注解就万事大吉了

%title插图%num

但是今天发现这样做在某些情况下会有bug,导致事务无法生效。

当这个方法被同一个类调用的时候,spring无法将这个方法加到事务管理中。

我们来看一下生效时候和不生效时候调用堆栈日志的对比。

%title插图%num

通过对比两个调用堆栈可以看出,spring的@Transactional事务生效的一个前提是进行方法调用前经过拦截器TransactionInterceptor,也就是说只有通过TransactionInterceptor拦截器的方法才会被加入到spring事务管理中,查看spring源码可以看到,在AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice方法中会从调用方法中获取@Transactional注解,如果有该注解,则启用事务,否则不启用。

%title插图%num

这个方法是通过spring的AOP类CglibAopProxy的内部类DynamicAdvisedInterceptor调用的,而DynamicAdvisedInterceptor继承了MethodInterceptor,用于拦截方法调用,并从中获取调用链。

如果是在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用,必须将方法放入另一个类,并且该类通过spring注入。

Solr的TrieField范围查询分析

solr从1.4版本开始,提供了一种字段类型TrieField(TrieLongField、TrieIntField等),用于范围查询,性能比普通的数值类型要快10倍。为什么会快那么多呢?网上找不到相关资料,通过分析源代码,大概了解了其原理,给大家分享下。

  • TrieField字段配置
    <fieldType name=”tint” precisionStep=”8” omitNorms=”true” positionIncrementGap=”0”/>

其中precisionStep代表字段值分段保存的时候,截断精度的大小。一般来说,其值越小,索引大小越大,查找速度越快。

  • TrieField索引
    TrieField字段在lucene中是用多个field来保存的,field的多少根据precisionStep决定,比如TrieIntField,precisionStep=”8”,则保存到索引中就是4个field,如图,32位的Int,每次缩进8位保存为一个field,新的field采用char数组来保存。因此索引的大小会比普通的IntField大。

%title插图%num

  • TrieField的范围查询:
    TrieField的范围查询通过高位范围匹配,低位边缘匹配,得到需要查询的term,再查询这些term得到docid来实现。如图:

%title插图%num

查找的过程:

1、将查找的范围A~B的上下界A、B值,取出*高8位,标记为A1、B1,到*段找在(A1~B1)内的term,得到需要查找的termlist1

2、继续取A、B值的*高16位,标记为A2、B2,到第二段来查在(A2~A1 11111111]和[B1 11111111,B2)范围内的Term,得到termlist2

3、继续取A、B值的*高24位,标记为A3、B3,到第三段来查在(A3~A2 11111111]和[B2 11111111,B3)范围内的Term,得到termlist3

4、继续取A、B值的*高24位,也即A、B值,到第四段来查找[A~A3 11111111]和[B3 11111111,B]范围内的Term,得到termlist4

5、*后查询这些term,归并,就得到了符合查询条件的docid了。从上面的描述,我们可以看到,需要查询的term*多为254+255*2+255*2+256*2=1786个,传统的方式A~B个term要小的多,因此性能有很大的提升。

Redis 客户端使用与监控

转载自:https://my.oschina.net/heweipo/blog/1536679

1、客户端通信协议

1)客户端与服务端之间的同学协议是在TCP协议之上构建的;

2)Redis定制了RESP(Redis Serialization Protocol ,Redis 序列化协议)实现客户端与服务端的正常交互。正因为这种协议简单而又容易理解,所以很多编程语言的客户端就容易实现了,比如 Java的客户端 Jedis.

 

2、客户端Jedis的使用

生产环境一般我们不会使用直连的方式(因为连接没法管理,导致资源不可控),而是使用连接池,使用 Jedis 连接池时主要注意一下几个方面。

1)maxActive:*大连接数,默认8个,生产环境一般配置16个差不多,(主要是Jedis处理高效)

2)maxIdle:池子里的*大空闲连接数,默认8个,意思是没有客户端来连接时池里*多闲置 8 个

3)minIdle:池子里的*小空闲连接数,默认0,意思是没有客户端来连接时池里*少闲置数

4)jmxEnabled:是否开启jmx监控,默认为 true,可以通过 jconsole 等工具监控连接池

5)minEvictableIdleTimeMillis:连接*小空闲时间,默认30分钟,达到这个值后空闲连接被移除

6)numTestsPerEvictionRun:做空闲连接检测时每次采样数,默认 3

7)TestOnBorrow:向连接池借用连接时是否做连接的有效性检测(Ping),默认false,如果设置为true,那么每次借用时都会多一次 ping,不过增强了代码的健壮性,其实如果是在Redis服务端配置了 timeout ,那么这个操作我觉得是很有必要的,因为服务端会把你这边的连接给移除。

8)testOnReturn:同上,不过这里是归还时,默认为false。

9)testWhileIdle:对连接池的连接时是否做空闲检测,默认为 false

10)timeBetweenEvicationRunsMillis:空闲连接检测周期,单位为毫秒,默认 -1 ,不做检测

11)blockWhenExhausted:当连接池用尽后,新的调用者是否要等待空闲连接,这个参数和下面的maxWaitMillis 对应的,只有当 blockWhenExhausted = true 时 maxWaitMillis 才会生效。默认为 true。

12)maxWaitMillis:当连接池资源用尽后,新的调用者来等待空闲连接时的*大时间,单位毫秒,默认 -1 ,表示永不超时,这个很要命的,一定要改掉。

 

3、客户端管理

1)查看客户端连接情况

>info clients

# Clients

connected_clients:2 // 当前连接数,这个暴增,说明连接池没有配置得当

client_longest_output_list:0 // 输出缓冲区列表*大对象数,其实就是*大 oll

client_biggest_input_buf:0 // *大输入缓冲区,单位字节,其实就是*大 qbuf

blocked_clients:0 // 正在阻塞阻塞命令(比如 blpop brpop 等)的客户端个数

 

2)查看服务端连接汇总

> info stats

# Stats

total_connections_received:6 // 从启动到现在连接的总数(并不是当前连接数)

rejected_connections:0 // 从启动到现在拒*的连接数

 

3)查看客户端连接的详细信息

>client list

id=5 addr=127.0.0.1:52821 fd=8 name= age=115 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=32768 obl=0 oll=0 omem=0 events=r cmd=client

 

id=6 addr=127.0.0.1:52822 fd=9 name= age=91 idle=10 flags=O db=0 sub=0 psub=0 multi=-1 qbuf=0 qbuf-free=0 obl=0 oll=0 omem=0 events=r cmd=monitor

 

id:唯一标识

name:客户端名称,通过 client setName client1 和 client getName 来设置获取名称

addr : 客户端的地址和端口,当想停止该客户端时则可以通过这个值 kill 掉

fd : 套接字所使用的文件描述符,Linux 里的 socket 句柄

age : 以秒计算的已连接时长,表示 client 从连接到现在的时长,所以叫做年龄

idle : 以秒计算的空闲时长,表示当前距离上一次执行命令的空闲时长

flags : 客户端 flag,标识客户端的信息,比如主从服务,事务执行情况等

db : 该客户端正在使用的数据库 ID

sub : 已订阅频道的数量

psub : 已订阅模式的数量

multi : 在事务中被执行的命令数量,如果没有使用事务就是 -1,记住是命令个数,包括读写

qbuf : 查询缓冲区的长度(字节为单位, 0 表示没有分配查询缓冲区)

qbuf-free : 查询缓冲区剩余空间的长度(字节为单位, 0 表示没有剩余空间)

obl : 输出缓冲区的长度(字节为单位, 0 表示没有分配输出缓冲区)

oll : 输出列表包含的对象数量(当输出缓冲区没有剩余空间时,命令回复会以字符串对象的形式被入队到这个队列里)

omem : 输出缓冲区和输出列表占用的内存总量

events : 文件描述符事件,w 可写, r 可读

cmd : *近一次执行的命令

 

注意:

A:输入缓冲区 qbuf 和 qbuf-free 以及 client_biggest_input_buf

Redis为每个客户端分配了输入缓冲区,它的作用是将客户端发送的命令临时保存,同时Redis会从输入缓冲区拉取命令并执行。其中 qbuf 代表这个缓冲区的总容量,qbuf-free 代表剩余容量。

Redis并没有提供相应的配置来规定每个缓冲区的大小,输入缓冲区会根据输入的内容大小的不同动态调整,只是要求每个客户端缓冲区的大小不能超过1G,超过后客户端将被关闭;另外如果没有达到 1 G,但是和当前Redis的空间加起来已经超过 MaxMemory 时,也可能出现 OOM,这个时候命令可能会被丢失。

其实生产环境很少有因为输入缓冲区导致的阻塞,主要还是因为命令过多积累在执行队列中,因此我们可以通过 qbuf 预测出这个连接有多少命令被发送,如果真的 qbuf 非常大,那么命令太多,可能会导致阻塞,比如管道、事务命令等。当然也有可能服务端发生阻塞导致客户端的命令一直被积压,也有可能客户端执行了大对象。

 

B:输出缓冲区 obl 和 oll 以及 client_longest_output_list

Redis为每个客户端分配了输出缓冲区,它的作用是保存命令执行的结果返回给客户端,其实就是为命令返回结果提供临时缓冲区。

与输入缓冲区不同的是,输出缓冲区的大小可以通过 client-output-buffer-limit 来进行设置。

输出缓冲区有两部分组成,固定缓冲区(obl)和动态缓冲区(oll),固定缓冲区只有 16KB,也就是返回值大于 16KB时,就会动用固定缓冲区,两种计算方式不一样哦,固定缓冲区是用字节单位,动态缓冲区使用 对象个数。另外 omem 是指动态缓冲区+固定缓冲区的总字节数。

生产环境遇到的问题在输出缓冲区还是比较多的,比如大对象的获取,就会造成 oll 占用过大,从而导致阻塞,所以*好是限制住 client-output-buffer-limit ,然后报警提示。

 

 

4)设置*大连接数 maxClients

> config get maxClients

1) “maxclients”

2) “10000”

> config set maxClients 500

OK

// *大连接数 10000,然而 Jedis的连接数默认是 8 ,对比一下差距好大啊,不知道这里如何设置的,是不是考虑到集群等因素。

 

5)设置检测客户空闲时间

> config get timeout

1) “timeout”

2) “30”

> config get timeout

1) “timeout”

2) “45”

// 如果client的idle得到了45,那么这个连接就会被kill掉,所以使用连接池时*好对连接进行可用性检测,同时也要避免客户端在一个连接内消耗大量的时间。

 

6)client setName | getName

> client setName client1

OK

> client getName

“client1”

// 给客户端指定一个自定义名字,方便查找。 我们可以在 client list 找到。

或者执行 > redis-cli client list | grep client1

 

7)client kill addr

>client kill 127.0.0.1:52875

>OK

 

8)client pause timeout(毫秒)

>client pause 1000

// 阻塞客户端 1000 毫秒,要知道Redis是单线程的,如果这个客户端阻塞,那么其他客户端全部都要等待他执行完成,所以一切客户端导致的延迟阻塞,都会是服务端的延迟阻塞。

 

 

9)monitor 监控其他客户端的命令执行

> monitor

OK

1503724607.774500 [0 127.0.0.1:52880] “get” “str”

1503724610.339557 [0 127.0.0.1:52880] “set” “str” “hll”

// 在寻找问题时这个命令很管用,但是高并发下,这个命令很致命的,因为大量输出导致该客户端的输出缓冲区被占满,从阻塞输出。所以用过后尽早关闭。

*个是执行命令的时间戳,可以使用 new Date(1503724607.774*1000)

第二个时数据库索引 0

第三个是执行的命令以及命令的参数 “get” “str”

 

 

10)客户端的其他配置

1)timeout :指Redis的连接如果空闲时间超过timeout则断开,可以通过 client list 查看连接

2)maxClients:这个和下面的 tcp 连接数不是一码事,这个指的是客户端数。

3)tcp-keepalive:检测TCP连接的活性周期,默认为 0 ,不检测。这个*好是配置一个 60 秒,因为大量的死连接会占用服务端的TCP资源。

4)tcp-backlog:TCP 三次握手成功后,会把这个连接放入队列中,这个队列的*大长度就是 tcp-backlog 的配置,默认是 511 , 通常这个参数不用调整。但是有些Linux系统的这个值配置时 128,则需要对应调整为 511.

如下查看和设置

> cat /proc/sys/net/core/somaxconn

128

> echo 511 > /proc/sys/net/core/somaxconn

> cat /proc/sys/net/core/somaxconn

511