背景
距离上一篇文章已经有4个多月了,这4个多月一直在忙着做一个数据库同步产品的代码研发和测试,现在基本运行稳定。 本文主要介绍一下,当时使用apache oro包进行正则过滤时,使用时出现的一个并发问题,排查了好几天才找到原因。希望大家使用时引以为戒,望周知。
过程
简单的描述下,我使用apache oro的场景: 进行数据库同步时,我们会根据定义的表名进行匹配,从binlog的数据流中提取出我们关心的表,然后进行解析,压缩,传输,写入目标库等一系列动作。
然而在线下测试环境中,冒出一个比较异常的情况,数据没有被正常的同步到目标库(概率发生的比较小,每次jvm重启后才可能出现),一节一节的往上查,最后定位是正则匹配时出的问题。
原因
出问题的代码: (这是当时出问题的代码,犯了多个错误)
1.public class RegexFunction extends AbstractFunction {
2.
3. private Map<String, Pattern> patterns = null;
4. private final PatternCompiler pc = new Perl5Compiler();
5.
6. public RegexFunction(){
7. patterns = new MapMaker().softValues().makeComputingMap(new Function<String, Pattern>() {
8.
9. public Pattern apply(String pattern) {
10. try {
11. return pc.compile(pattern, Perl5Compiler.CASE_INSENSITIVE_MASK);
12. } catch (MalformedPatternException e) {
13. throw new CanalSinkException(e);
14. }
15. }
16. });
17. }
18.
19. public AviatorObject call(Map<String, Object> env, AviatorObject arg1, AviatorObject arg2) {
20. String pattern = FunctionUtils.getStringValue(arg1, env);
21. String text = FunctionUtils.getStringValue(arg2, env);
22. Perl5Matcher matcher = new Perl5Matcher();
23. boolean isMatch = matcher.matches(text, patterns.get(pattern));
24. return AviatorBoolean.valueOf(isMatch);
25. }
26.
27. public String getName() {
28. return "regex";
29. }
30.
31.}
说明:
- 使用了aviator做为表达式引擎( http://code.google.com/p/aviator/),扩展了一个regex表达式语法的支持。(默认是自带了jdk pattern的正则)
- 使用google collection,将pattern进行lazy-init处理,多线程共享。(map中没有对应pattern,就进行compiler)
错误1:
1. Perl5Compiler是有状态的,会在compiler过程中产生一些上下文状态。 (之前先入为主的认为类似于Compiler基本都是单例使用,线程安全,基本我自己写代码和命名是这个习惯)
对应的Perl5Compiler的javadoc: (已经比较明确的指出,Perl5Compiler和Perl5Matcher是非线程安全的)
我做了个测试:
1.public class MutliAviaterFilterTest {
2.
3. @Test
4. public void test_simple() {
5. int count = 5;
6. ExecutorService executor = Executors.newFixedThreadPool(count);
7.
8. final CountDownLatch countDown = new CountDownLatch(count);
9. final AtomicInteger successed = new AtomicInteger(0);
10. for (int i = 0; i < count; i++) {
11. executor.submit(new Runnable() {
12.
13. public void run() {
14. try {
15. for (int i = 0; i < 100; i++) {
16. doRegexTest();
17. // try {
18. // Thread.sleep(10);
19. // } catch (InterruptedException e) {
20. // }
21. }
22.
23. successed.incrementAndGet();
24. } finally {
25. countDown.countDown();
26. }
27. }
28. });
29. }
30.
31. try {
32. countDown.await();
33. } catch (InterruptedException e) {
34. }
35.
36. Assert.assertEquals(count, successed.get());
37. executor.shutdownNow();
38. }
39.
40. private void doRegexTest() {
41. AviaterRegexFilter filter3 = new AviaterRegexFilter("otter2.otter_stability1|otter1.otter_stability1|"
42. + RandomStringUtils.randomAlphabetic(200));
43. boolean result = filter3.filter("otter1.otter_stability1");
44. Assert.assertEquals(true, result);
45. result = filter3.filter("otter2.otter_stability1");
46. Assert.assertEquals(true, result);
47. }
48.
49.}
说明:
- 每次构造一条正则(固定的字符串 + 一段200个字符的随机值)
- 多线程并发的进行compiler , matcher操做
结果:
- 单线程测试顺利通过
- 多线程测试,没加200字符的随机值,尝试到20,30并发都没有发现failed
- 多线程测四和,正则表达式加上200字符的随机值,尝试2个并发就遇到了failed情况
看来需要有足够的碰撞才行,通过增加表达式长度,增加了compiler的编译时间,增加了多线程碰撞的几率,从而造成编译出的Pattern是一个不正确的状态机。
错误2:
2. Pattern在不同的参数编译下,会有不同的结果,也会有并发问题。 (对应javadoc中也已经有了说明)
我也做了个类似测试,在20,30的并发度下,也没有发现failed情况。后续可以在更高的压力,更复杂的正则表达式进行匹配,估计碰撞的概率就会比较高
总结
正确的代码写法:
1.public RegexFunction(){
2. patterns = new MapMaker().softValues().makeComputingMap(new Function<String, Pattern>() {
3.
4. public Pattern apply(String pattern) {
5. try {
6. <span style="color: rgb(255, 0, 0);">PatternCompiler pc = new Perl5Compiler();</span>
7. return pc.compile(pattern, Perl5Compiler.CASE_INSENSITIVE_MASK<span style="color: rgb(255, 0, 0);"> | Perl5Compiler.READ_ONLY_MASK</span>);
8. } catch (MalformedPatternException e) {
9. throw new CanalSinkException(e);
10. }
11. }
12. });
13. }
看来在使用一些三方库时要多注意一些细节,尽管你之前已经对该三方库使用已经比较久,但未必不会出问题,只不过是你的运气好与不好而已。