前言
因为需要搞定项目中大大小小所有坑,所以总是有些东西需要记录下来方便备查。另外就是原先没有记录下来的点,搞清楚了也需要记录下来,或者暂时还存疑但是问题解决了等等。按说我整个博客大部分都是这种东西,但是很多东西在最初不方便归类,但是如果因为这样就不记录了,那也就会失去这份记录。所以,这里就是他们最初的记录低点。等到多了,或者成系统了在单独开贴总结。
spring接收json序列化与反序列化
spring在接收json数据转换成对象的时候,是通过jackson进行发序列化的。默认配置足以应对大部分情况,不过还有部分常用的配置需要自己来进行。我之前的博客有记录,看了下这里没有,而且这次遇到了新坑,故整理在此。先上配置代码吧。
MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
SimpleModule simpleModule = new SimpleModule();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
objectMapper.setDateFormat(dateFormat);
//不显示为null的字段
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
simpleModule.addSerializer(Long.class, ToStringSerializer.instance);
simpleModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(simpleModule);
jackson2HttpMessageConverter.setObjectMapper(objectMapper);
//忽略多余的属性,而不是报错
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//放到第一个
converters.add(0,jackson2HttpMessageConverter);
当初整理这个的初衷是jackson换api了,网上找的到处都是被抛弃的api的配置。简单说下这里做的事情。
- 日期对象格式化:我不喜欢传它的默认时间,不好用也不能直接展示。所以做了自定义序列化。
- 对于null数据的处理:这里的配置是如果这个数据为null,就去掉它。根据需要自己配置吧,无疑,更加可控更重要。
- Long类型的转换:这个是实际填坑填出来的,因为long传到web前端后,第四位会被抹零。这东西不报错,所以很恶心。如果你的id是long,传到列表里,再用get获取详情就会找不到,因为低位被抹零了。这里我们直接转换成了字符串。
- 忽略多余的属性:最后这个最新填的坑。是在同一个弹窗做添加和编辑的时候,由于数据模型差一个id属性。结果,多传的属性就导致json反序列化报错。
使用redis有序集合解决区间检索的问题
问题来源
我要解决的问题是,使用产品编号找到其对应的产品型号的问题。按理说,这个东西没啥问题。问题就在,如果按照之前的业务逻辑,我想知道产品型号就需要先在系统中登记产品信息,这个时候就有了产品型号和产品编号的关联。但是,在产品生产测试的时候,其实就有可能会用到这个查找,而这个时候是没有在系统中登记产品信息的。如果为了这件事就去一个一个登记产品信息,人力成本就有些高了。也有常见的解决方案是在产品编号中加入特定的头以区分其型号。但是,由于产品编号需要在物联网设备中传输,我们的编码是自定义的。产品编号是固定的四字节整数,如果希望在这里划分产品型号的空间的话,产品编号的利用率就会降低不少,所以也被排除了。
最后的解决方案是,我们在提供中建立一个产品批次的管理。每次生产都需要划分正式的产品编号以确保生产产品是不会重号的。而每个批次,都会对应一个产品型号。这就给我们的映射提供了基础数据。剩下的问题就变成了,如何从产品批次中检索到我们需要的批次并获取其型号信息的事情了。
解决方法
产品批次的信息有用的包含如下这些:
- 记录编号:这是产品批次的唯一编号
- 产品型号:用来记录这个批次的产品型号是多少
- 起始编号:用来记录这个批次最小的产品编号是多少
- 截止编号:用来记录这个批次最大的产品编号是多少。由于起始和截止编号都是被包含的,所以这两个编号是可以重复的。
我想要做的事,就是给我一个产品编号,我能通过批次记录找到其对应的型号。
思路历程
解决这个问题的思路我大概经历了三次重要的变迁,我在这里都记录下来,我觉得它们在不同的时候都有其使用价值。
直觉上告诉我,这个问题可以通过redis的有序集合来解决,但是对应对的API我没用过。看了几本的API后,我想我可以用起始编号当作score,value就存储起始编号和截止编号的json序列化的字符串。然后我就去研究API去了。
这个时候其实我是对在排序集合里面存储json对象表示不爽。所以,我想要在里面存储id,然后单独用一个hash来维护批次号的缓存,可能还会在别的业务上有用。虽然没啥卵用吧,不过,这个思路在用redis的过程中应该是用得上的。
这个时候,其实我突然想到,既然我要做的就只是查询到产品型号,那为什么我那个里面存储的不能是产品型号呢?似乎是可以的,score是起始编号,那下一个score就是截止编号后面那个嘛。说实话,差点被这个想法骗到。虽然我现在的编号确实都是连续的,但是这并不是强约束。所以,还是需要记录截止编号。所以,最终,里面存的是产品型号和截止编号的json序列化值。而这个排序,就仅作该用途之用。
技术关键点
首先是添加数据进去,用到的命令是zadd,如下。
ZADD KEY_NAME SCORE1 VALUE1.. SCOREN VALUEN
zadd可以一次添加多个条目进去,成员已经存在的话,就是修改分数。这个命令很简单,属于维护用的,删除的我就不写了。
然后是,怎么判断哪个里面有给定的产品编号。用到的命令是zrangebyscore,格式如下:
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]
注意它里面的参数,可以指定一个score的区间,返回区间内的数据。如果withscores,就是连score一起返回。后面的limit是分页参数,各大文档都没细讲它是怎么回事,其实和mysql的limit一样。需要指出的是,这里没有办法指定排序规则,而他总是从小到大排序。所以,我min设置为0,max设置为产品编号,就会得到期间所有的记录。而其实最后一条才是我要的,但是这里无法直接拿最后一条。
最后,我想到了一个实现思路,要用到另一个指令,zcount
ZCOUNT key min max
用这个帮我数一下有多少,我就可以用它来指定分页数据,就可以只拿最后一条。这样可以减小很多数据传输的量。而这两个操作的时间复杂度都是logn,还是很不错哒。
产线排查记录
集合遍历删除bug
产线一直存在不定期重启的问题,一直没有找到真正的原因。抓到过几个产线的bug,但是似乎都没有效果。直到仅限我发现,tcp连接移除的时候,报了一个java.util.ConcurrentModificationException: null。查了下,这个异常是遍历集合时移除集合内容报的错。看了下代码,真的是for循环map的value集合,然后移除了里面的对象。查了下历史,一个已经离职的同事写的。这东西,怎么说呢?基本功的问题吧。不过说实话,重写循环的时候一下子我也卡壳了,现在我是通过遍历key集合来操作的,这样就不会引发这个错误了。这里算是一个小知识点吧,总结一下。
使用EntrySet及values进行遍历
这两个遍历都可以达到遍历的效果,但是遍历的过程中是不可以对自己和元素进行移除的。移除会报错,就是上面的错。至于为什么,客观自己拔下源代码吧,我现在没有时间扒它。
使用iterator迭代
网上盛传的迭代过程中移除元素的方法就是这个迭代器迭代。但是,我个人不是很喜欢用它。因为,它其实并不是想象中那么好用。如果在迭代的过程中,你直接使用map的实例移除元素一样是会报错的。只有使用iterator自己的remove方法才能正确删除当前元素,所以并不好用。比如,我这次的场景,虽然是在遍历过程中进行的,但是其实是在一个子方法中进行的,如果要用这种移除就需要把迭代器的实例传过去,这就很丑了。
使用keyset进行遍历
经过测试,遍历keyset并不会影响map实例的移除操作,是一种很实用的方法,至于网上测评比较慢的说法,说实话这种场景下我是愿意接受的。
index遍历(不适用Map)
其实最好用的还是用序号进行遍历。不过map的存储结构决定了这不适用与它,不过list是很好用的。如果要移除的话,注意从高到低遍历。