mysql在将字符串与整数比较时存在一个坑:
select * from xxxx
where xx_id =97
得到下面的数据,可以看到第2列是字符串类型,明显与整数不相等,但是mysql却当做相等查询了出来,mysql在处理字符串与整形数据对比时,会依次将字符串与整形数据对比,直到字符串不为整数的字母为止!
所以我们在使用mysql时一定要注意这一点,使用同类型数据对比!
云计算
mysql在将字符串与整数比较时存在一个坑:
select * from xxxx
where xx_id =97
得到下面的数据,可以看到第2列是字符串类型,明显与整数不相等,但是mysql却当做相等查询了出来,mysql在处理字符串与整形数据对比时,会依次将字符串与整形数据对比,直到字符串不为整数的字母为止!
所以我们在使用mysql时一定要注意这一点,使用同类型数据对比!
使用 vuejs 做了一个简单的功能页面,逻辑是,页面加载后获取当前的经纬度,然后通过 ajax 从后台拉取附近的小区列表。但是 bug 出现了,在显示小区列表之前,会闪现小区名对应的 vuejs 变量名。
案发现场的 HTML 代码
页面加载时,会闪现
{{ item.name }}
Google 了一下,发现 vuejs 内置的 directive v-cloak 可以解决这个问题。非常简单
HTML 修改成
CSS 中添加
搞定!
但是原理是什么呢?
这段 CSS 的含义是,包含 v-cloak (cloak n. 披风,斗篷;vt. 遮盖,掩盖) 属性的 html 标签在页面初始化时会被隐藏。
在 vuejs instance ready 之后,v-cloak 属性会被自动去除,也就是对应的标签会变为可见
把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的这个方法显式的指定误判率:
private static BloomFilter<Integer> bloomFilter = BloomFilter.create(Funnels.integerFunnel(), capacity,0.01);
我们断点跟踪下,误判率为0.02和默认的0.03时候的区别:
对比两个出错率可以发现,误判率为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中的事务,留给各位看官自己思考探究。
一直使用json游离于各种编程语言和系统之间。一个偶然的机会碰到了Fastjson,被他的无依赖、易使用、应用广等特性深深吸引的同时,更被他出奇的“快”所震惊,在Java界犹如一骑*尘,旁人只能望其项背。很自然的一个想法涌上心头:FastJSON为何如此之快?于是定神来拔一拔其实现,一则膜拜大师的杰作,二则虚心偷技,三则方便来者学习。
本篇接下来的内容是基于FastJSON 1.1.40,着重讲述其序列化、反序列化实现,*后分析FastJSON为何如此“fast”的原因。
所谓序列化,就是将java各种对象转化为json串。不多说,先上图。
平常我们经常用到的是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,完成高性能的字符串拼接。该类成员如下:
可理解为每次序列化后字符串的内存存放地址。
每次序列化,都需要重新分配buf[]内存空间。而bufLocal就是每次序列化后bug[]的内存空间保留到ThreadLocal里,但其中的值清空,避免频繁的内存分配和gc。
生成json字符串的特征配置,默认配置为:
<span>QuoteFieldNames | SkipTransientField | WriteEnumUsingToString | SortField</span>
表示含义为:双引号filedName and 忽略transientField and enum类型使用String写入 and 排序输出field。 支持的所有特征在SerializerFeature类中,用户可在调用时显示配置,也可通过JSONFiled或JSONType注入配置。
writer 用户指定将生成的json串直接写入某writer中,比如JSONWriter类。
举个例子吧,writeStringWithDoubleQuote()表示用字符串用双引号写入,看看如何拼接字符串的。
3. Filter列表
SerializeWriter中有很多Filter列表,可视为在生成json串的各阶段、各地方定制序列化,大致如下:
4. DateFormat
指定日期格式。若不指定,FastJSON会自动识别如下日期格式:
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接口的类),大致逻辑如下:
整个过程是先获取已实现基础类对应的序列化类,再通过类加载器获取自定义的AutowiredObjectSerializer序列化类,*后获取通过createJavaBeanSerializer()创建的序列化类。通过该方法会获取两种序列化类,一种是直接的JavaBeanSerializer(根据类的get方法、public filed等JavaBean特征序列化),另一种是createASMSerializer(通过ASM框架生成的序列化字节码),优先使用第二种。选择JavaBeanSerializer的条件为:
结合前面的讨论,可以得出使用ASM的条件:非Android系统、非基础类、非自定义的AutowiredObjectSerializer、非以上所列的使用JavaBeanSerializer条件。
具体基础类的序列化方法、JavaBeanSerializer的序列化方法和ASM生成的序列化方法可以参见代码,这里就不做一一讲解了。
所谓反序列化,就是将json串转化为对应的java对象。还是先上图。
同样是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的某些成员:
由于json串具有一定格式,字符串会根据某些特定的字符来自解释所表示的意义,那么这些特定的字符或所处位置的字符在FastJSON中就叫一个token,比如”(“,”{“,”[“,”,”,”:”,key,value等,这些都定义在JSONToken类中。
解析器通过扫描输入字符串,将匹配得到的*细粒度的key、value会放到sbuf中。
上面sbuf的空间不释放,在下次需要的时候直接拿出来使用,从避免的内存的频繁分配和gc。
反序列化特性的配置,同序列化的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对。当然,这些参数也是可以通过其他途径配置的。
对转义符的处理,比如’\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的条件如下:
使用ASM生成的反序列化器具有较高的反序列化性能,比如对排序的json串可按顺序匹配解析,从而减少读取的token数,但如上要求也是蛮严格的。综上,FastJSON反序列化也支持基础反序列化器、JavaBeanDeserializer反序列化器和ASM构造的反序列化器,这里也不做一一讲解了。
FastJSON真的很快,读后受益匪浅。个人总结了下快的原因(不一定完整):
1. 专业的心做专业的事
不论是序列化还是反序列化,FastJSON针对每种类型都有与之对应的序列化和反序列化方法,就针对这种类型来做,优化性能自然更具针对性。自编符合json的SerializeWriter和JSONLexer,就连ASM框架也给简化掉了,只保留所需部分。不得不叹其用心良苦。
2. 无处不在的缓存
空间换时间的想法为程序员屡试不爽,而作者将该方法用到任何细微之处:类对应的序列化器/反序列化器全部存起来,方便取用;解析的key存起来,表面重复内存分配等等。
3. 不厌其烦的重复代码
我不知道是否作者故意为之,程序中出现了很多类似的代码,比如特殊字符处理、不同函数对相同token的处理等。这样虽对于程序员寻求规整相违背,不过二进制代码却很喜欢,无形之中减少了许多函数调用。
4. 不走寻常路
对于JavaBean,可以通过发射实现序列化和反序列化(FastJSON已有实现),但默认使用的是ASM框架生成对应字节码。为了性能,无所不用其*。
5. 一点点改变有很大的差别
排序对输出仅是一点小小的改变,丝毫不影响json的使用,但却被作者用在了解析的快速匹配上,而不用挨个拎出key。
6. 从规律中找性能
上面也讲到,FastJSON读取token基于预测的。json串自身的规律性被作者逮个正着,预测下一个将出现的token处理比迷迷糊糊拿到一个token再分情况处理更快捷。
本文实验的测试环境: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
四、用例子说明各个隔离级别的情况
1、读未提交:
(1)打开一个客户端A,并设置当前事务模式为read uncommitted(未提交读),查询表account的初始值:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,虽然客户端B的事务还没提交,但是客户端A就可以查询到B已经更新的数据:
(4)一旦客户端B的事务因为某种原因回滚,所有的操作都将会被撤销,那客户端A查询到的数据其实就是脏数据:
(5)在客户端A执行更新语句update account set balance = balance – 50 where id =1,lilei的balance没有变成350,居然是400,是不是很奇怪,数据不一致啊,如果你这么想就太天真 了,在应用程序中,我们会用400-50=350,并不知道其他会话回滚了,要想解决这个问题可以采用读已提交的隔离级别
2、读已提交
(1)打开一个客户端A,并设置当前事务模式为read committed(未提交读),查询表account的所有记录:
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account:
(3)这时,客户端B的事务还没提交,客户端A不能查询到B已经更新的数据,解决了脏读问题:
(4)客户端B的事务提交
(5)客户端A执行与上一步相同的查询,结果 与上一步不一致,即产生了不可重复读的问题
3、可重复读
(1)打开一个客户端A,并设置当前事务模式为repeatable read,查询表account的所有记录
(2)在客户端A的事务提交之前,打开另一个客户端B,更新表account并提交
(3)在客户端A查询表account的所有记录,与步骤(1)查询结果一致,没有出现不可重复读的问题
(4)在客户端A,接着执行update balance = balance – 50 where id = 1,balance没有变成400-50=350,lilei的balance值用的是步骤(2)中的350来算的,所以是300,数据的一致性倒是没有被破坏。可重复读的隔离级别下使用了MVCC机制,select操作不会更新版本号,是快照读(历史版本);insert、update和delete会更新版本号,是当前读(当前版本)。
(5)重新打开客户端B,插入一条新数据后提交
(6)在客户端A查询表account的所有记录,没有 查出 新增数据,所以没有出现幻读
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
这里面有几点需要大家留意:
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注解就万事大吉了
但是今天发现这样做在某些情况下会有bug,导致事务无法生效。
当这个方法被同一个类调用的时候,spring无法将这个方法加到事务管理中。
我们来看一下生效时候和不生效时候调用堆栈日志的对比。
通过对比两个调用堆栈可以看出,spring的@Transactional事务生效的一个前提是进行方法调用前经过拦截器TransactionInterceptor,也就是说只有通过TransactionInterceptor拦截器的方法才会被加入到spring事务管理中,查看spring源码可以看到,在AdvisedSupport.getInterceptorsAndDynamicInterceptionAdvice方法中会从调用方法中获取@Transactional注解,如果有该注解,则启用事务,否则不启用。
这个方法是通过spring的AOP类CglibAopProxy的内部类DynamicAdvisedInterceptor调用的,而DynamicAdvisedInterceptor继承了MethodInterceptor,用于拦截方法调用,并从中获取调用链。
如果是在同一个类中的方法调用,则不会被方法拦截器拦截到,因此事务不会起作用,必须将方法放入另一个类,并且该类通过spring注入。
solr从1.4版本开始,提供了一种字段类型TrieField(TrieLongField、TrieIntField等),用于范围查询,性能比普通的数值类型要快10倍。为什么会快那么多呢?网上找不到相关资料,通过分析源代码,大概了解了其原理,给大家分享下。
其中precisionStep代表字段值分段保存的时候,截断精度的大小。一般来说,其值越小,索引大小越大,查找速度越快。
查找的过程:
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要小的多,因此性能有很大的提升。
转载自:https://my.oschina.net/heweipo/blog/1536679
1、客户端通信协议
1)客户端与服务端之间的同学协议是在TCP协议之上构建的;
2)Redis定制了RESP(Redis Serialization Protocol ,Redis 序列化协议)实现客户端与服务端的正常交互。正因为这种协议简单而又容易理解,所以很多编程语言的客户端就容易实现了,比如 Java的客户端 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 ,表示永不超时,这个很要命的,一定要改掉。
>info clients
# Clients
connected_clients:2 // 当前连接数,这个暴增,说明连接池没有配置得当
client_longest_output_list:0 // 输出缓冲区列表*大对象数,其实就是*大 oll
client_biggest_input_buf:0 // *大输入缓冲区,单位字节,其实就是*大 qbuf
blocked_clients:0 // 正在阻塞阻塞命令(比如 blpop brpop 等)的客户端个数
> info stats
# Stats
total_connections_received:6 // 从启动到现在连接的总数(并不是当前连接数)
rejected_connections:0 // 从启动到现在拒*的连接数
>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 ,然后报警提示。
> config get maxClients
1) “maxclients”
2) “10000”
> config set maxClients 500
OK
// *大连接数 10000,然而 Jedis的连接数默认是 8 ,对比一下差距好大啊,不知道这里如何设置的,是不是考虑到集群等因素。
> config get timeout
1) “timeout”
2) “30”
> config get timeout
1) “timeout”
2) “45”
// 如果client的idle得到了45,那么这个连接就会被kill掉,所以使用连接池时*好对连接进行可用性检测,同时也要避免客户端在一个连接内消耗大量的时间。
> client setName client1
OK
> client getName
“client1”
// 给客户端指定一个自定义名字,方便查找。 我们可以在 client list 找到。
或者执行 > redis-cli client list | grep client1
>client kill 127.0.0.1:52875
>OK
>client pause 1000
// 阻塞客户端 1000 毫秒,要知道Redis是单线程的,如果这个客户端阻塞,那么其他客户端全部都要等待他执行完成,所以一切客户端导致的延迟阻塞,都会是服务端的延迟阻塞。
> 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”
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