关于BeanCopier的使用

在做业务系统开发时,我们通常分为三层,一层时对外的服务接口层,一层时内部业务逻辑层,一层是数据存储层,没层都会定义自己的类,但是这三层需要交互,交互就会涉及到数据传输,数据传输就会涉及到数据转换,因为我们每层都定义自己类来处理相关的逻辑。我们通常会把业务层的对象转换成持久层的对象再去做持久化存储,对象之间的转换我们通常使用BeanCopier来操作,但是在实际项目中发现大家使用BeanCopier的方式也不统一,有些人喜欢用Converter,有些人不喜欢用Converter,其实关于Converter我一般也不会用,都是自己先使用一把BeanCopier然后再把copy不了的字段手工填写一遍。关于BeanCopier如何使用,曾经看到有人写过下面这个Utils,代码简化了很多,避免定义很多BeanCopier。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class BeanCopierUtils {  
    // BeanCopier的内存缓存 
    public static Map<String, BeanCopier> beanCopierMap = new HashMap<String, BeanCopier>();  
        
    public static void copyProperties(Object source,Object target){  
        String beanKey = generateKey(source.getClass(),target.getClass());  
        BeanCopier copier = null;  
        if (!beanCopierMap.containsKey(beanKey)) {  
            copier = BeanCopier.create(source.getClass(), target.getClass(), false);  
            beanCopierMap.put(beanKey, copier);  
        }else {  
            copier = beanCopierMap.get(beanKey);  
        }  
        // 没有使用Converter
        copier.copy(source, target, null);  
    }  
    // BeanCopier缓存的Key
    private static String generateKey(Class<?>class1,Class<?>class2){  
        return class1.toString() + class2.toString();  
    }
}

mysql中select for update锁的问题

在日常关于资金业务的开发过程中,涉及到数据库的操作时我们经常按照一查二锁三写的套路,可能在加锁后还需要查询一些数据,因为加锁之前的数据可能已经发生变化了,这里的锁我们一般会采用数据库的select for update锁,使用这个本身也没有问题,在对数据进行操作的时候先加锁,避免其他线程修改后,当前线程看到的数据是错误的。但是在高并发的情况下面,这个select for update锁的等待时间太久导致很多线程都在等待这个锁,这样整个系统的吞吐量严重下降。举个栗子在电商业务中有个系统自动确认收货,确认收货后会更改一些用户的数据,如果同卖家在双11当天同时卖出了上万件商品,生成了上万笔订单,这上万笔订单系统自动确认收货的时间点都是一样的,到点后会爆发非常大的并发流量,如果先select for update的话,很多线程就会排队等待,系统吞吐量下降,数据库链接数耗尽,出现故障。

在高并发系统中,如果出现了获取不到锁的情况,应该尽快返回让上游系统发起重试,而不是一直等直到可以获取到锁,一直的等待绝对会把系统拖死,这种case目前已经经历了好多了。

初识EventSourcing和CQRS

EventSourcing就是事件溯源的意思,我们平时在设计系统的时候都存储了对象的最终的状态,比如一个交易订单,它当前的状态是等待买家确认收货,这个状态是由于发生了一系列事件所导致的,那么EventSourcing的思想就是存储对象的所有变更事件,根据对象的所有变更事件追溯出对象的状态。

1
state = f(E1,E2,E3....)

函数f就是溯源函数,这种设计思想对于数据一致性也有保证,因为它对所有的变更都是insert,即追加记录,能够很好地避免RaceCondition。不同的对象可能需要定义不同的溯源函数,由于数据库中没有存储对象当前的状态,那么需要根据一些状态来进行遍历的时候就比较麻烦了。

CQRS是一种读写分离的架构设计,CQRS最早来自于Betrand Meyer(Eiffel语言之父,开-闭原则OCP提出者)在 Object-Oriented Software Construction 这本书中提到的一种 命令查询分离 (Command Query Separation,CQS) 的概念。其基本思想在于,任何一个对象的方法可以分为两大类:

1
2
命令(Command):不返回任何结果(void),但会改变对象的状态。
查询(Query):返回结果,但是不会改变对象的状态,对系统没有副作用。

先贴一张CQRS的架构图
CRQS
一个Command只能修改一个聚合根,对同一个聚合根的修改需要通过Command Bus来排队。一个聚合根的修改可能会导致多个领域模型状态上的变更,这些状态的变更全部以事件的方式存储,事件通过EventBus找到对应的Handler,然后更新DB中的数据。其实上面这个架构可以理解为为是CQRS和EventSouring的结合。

CQRS参考资料

两种分布式系统中保持数据一致性的设计方案

CAP是分布式系统中非常重要的理论依据,任何分布式系统设计的时候都要考虑一下CAP。

1
2
3
C(一致性):所有节点上的数据时刻保持同步
A(可用性):每个请求都能得到一个响应,不论成功还是失败
P(分区容错):系统能够持续提供服务,即使系统内部有消息丢失

CAP只能满足两条,现在大多数业务系统都是AP without C。
在平时的业务开发中,业务系统A更新完自己的数据后,还需要业务系统B更新相关的数据,要么系统A发消息给系统B,系统B受到消息后更新自己的数据,要么系统A通过RPC接口更改系统B的数据。那么如何保证系统A,B的数据一致性呢?如果要强一致的话,就需要分布式事务,但是分布式事务本身有一定的开发成本,同时也会影响系统的稳定性。
不使用分布式事务如何来保证一致性呢?只能放弃强一致性,变成最终一致性。一般会有两种方案,一种是通过消息中间件来实现,一种是利用数据库事务加上异步轮询来处理。

【通过消息中间件来实现】
消息中间件
这种方案需要相应的消息中间件来支持。
【利用数据库事务+异步轮询】
事务轮询
业务系统A更新完自己的数据后,创建一个事件,创建事件和更新自己业务数据放在一个事务里面,这样业务系统A数据更新和事件创建会一起完成,剩下的就是保证业务事件A一定要处理,要么通过RPC调用业务系统B的接口,要么发消息给业务系统B。

mysql中的null

最近在优化系统代码的时候,需要修改一下表结构,增加了一个字段D,原来表中A,B,C三字段构成了唯一性约束,现在增加了D字段,D字段是可控的,没有缺省值。同时新的唯一性约束由A,B,C,D四个字段构成,上线后发现唯一性约束失效啦。
a1,b1,c1,null插入数据库后,a1,b1,c1,null还可以插入数据库。也就是说mysql任务[a1,b1,c1,null]和[a1,b1,c1,null]是两条记录,的确是这样,在mysql中null和null是不相等的。

mysql相关的注意点

最近一直忙于业务系统的开发,在开发过程中少不了sql语句的使用,我们在分页遍历的时候经常需要排序,就会编写如下的sql

1
2
3
select x, y, z from tableX where status=#status#
order by apply_time
limit 0,100

这个分页sql看上去没有什么问题,但是在执行过程中发现有数据被重复遍历出来,然后仔细排查发现上面sql中的order by apply_time是存在问题的,因为多条记录如果apply_time是一样的话,会被重复扫描到,排序字段一样,同一条记录可能会出现在不同的页中。在编写mysql分页遍历语句时,一定要注意排序字段的选择,主要重复筛选的问题。

现在整个业界对于分布式锁暂时还没有一些好的做法,我自己习惯使用mysql的select for update来实现锁,但是在使用select for update时,如果where语句中不带主键id的话,该sql可能会锁表,因为mysql发现where语句匹配到的记录数目比较多,对多行加锁的代价比对单表加锁的代价高,因此mysql会对表加锁。使用select for update最安全的做法是带上主键ID。如果要加锁多行,写个for循环对多条记录按照固定的顺序依次执行select for update。

Spring的一些小事

最近有同事看到下面的代码

1
2
3
4
5
6
public class A implements IA {
private B b;
public void setB(B b){}
}
public class B implements IB {
}

Spring的配置文件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
default-autowire="byName">
<bean id="a" class="A" />
<bean id="b" class="B" />
</beans>

这样的代码在运行时a这个bean中的b能注入吗?因为setter方法的注入需要在配置文件中显示去写属性的

1
2
3
<bean id="a" class="A">
<property name="b" ref="b"/>
</bean>

需要这样写才可以,为啥不写也可以呢?我当时也看了半天,最后发现是XML头部schema中的

1
default-autowire="byName"

导致。很简单一个写法,但是一般我们不会直接在XML的头部带上这个的。

另外最近又重温了一遍Spring的源码,发现了下面这个接口中两个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface ConfigurableListableBeanFactory
extends ListableBeanFactory, AutowireCapableBeanFactory, ConfigurableBeanFactory {

/**
* Ignore the given dependency type for autowiring:
* for example, String. Default is none.
* @param type the dependency type to ignore
*/
void ignoreDependencyType(Class type);

/**
* Ignore the given dependency interface for autowiring.
* <p>This will typically be used by application contexts to register
* dependencies that are resolved in other ways, like BeanFactory through
* BeanFactoryAware or ApplicationContext through ApplicationContextAware.
* <p>By default, only the BeanFactoryAware interface is ignored.
* For further types to ignore, invoke this method for each type.
* @param ifc the dependency interface to ignore
* @see org.springframework.beans.factory.BeanFactoryAware
* @see org.springframework.context.ApplicationContextAware
*/
void ignoreDependencyInterface(Class ifc);
....
}

看上面的注释不是很明白,ignoreDependencyType和ignoreDependencyInterface,从方法名来看一个是忽略某些类的依赖,一个是忽略某些接口的依赖。什么意思呢?在Spring中我们经常使用的是面向接口的编程,也就是在自动注入中,如果发现接口或者类被ignoreDependency了,就不会自动注入了。比如说你不能自动注入BeanFactory和ApplicationContext,它们必须通过BeanFactoryAware和ApplicationContextAware来注入,其实直接看英文注释也非常简单。

nginx线上出错

最近在做nginx的升级,很多网站的架构都是代理服务器(Apache/Nginx)加上应用容器(Tomcat/Jetty/Jboss),Nginx在性能上由于Apache,在升级的过程中出现了下面的错误

1
nginx: [emerg] could not build the variables_hash, you should increase either variables_hash_max_size: 512 or variables_hash_bucket_size: 64

查找Nginx的文档,找到解决方案

1
2
3
4
5
6
http  {
    ......
    variables_hash_max_size 51200;
    variables_hash_bucket_size 6400;
    ......
}

在http的配置中增加上面两个配置即可。
另外,对于基础软件的升级一定要注意操作上的顺序,本次升级过程中发现之前老的一些配置都不兼容,如果操作不当很容易引发线上故障。

JAVA单元测试mock框架

一.概述

最近在做代码重构,发现系统中的UT很少,重构没有UT的话,全部得人工测试,逻辑覆盖不一定全部能覆盖到,因此UT还是很有必要存在的。在写UT的时候,mock是必须要有的,但是现在适用于java代码做单元测试的mock框架很多,我们该如何选择?