问题起因
上篇文章我们说到了一个MySQL的死锁问题(传送门:一次线上Mysql死锁引发的血案),当时案例1已经找到原因,但是案例2依然无解(是的,我现在依然无解o(╯□╰)o)。但是按照我们的线上问题首要原则,我们必须第一时间解决问题。
第一时间解决问题,保留现场事后分析。
没找到原因,可以先解决问题吗?当然可以。
虽然对于死锁是怎么发生的,我们暂时还不清楚。但是从当时的死锁日志来看,我们知道,发生死锁是因为有两个事务以不同的加锁顺序分别对主键和idxpaynokdt进行加锁,导致两个事务分别持有对方所需要的锁,并且等待对方释放锁引起的。
因此DBA给了一个建议,就是在事务1中,先查出需要更新的记录的主键,然后根据主键进行更新。在这种更新策略下,事务1就不会先持有idxpaynokdt上的锁,而是会直接对主键进行加锁,如此一来就破坏了造成死锁的条件。
第一次尝试
看起来似乎很简单,我三下五除二就改好了代码。然而,下单支付之后发现,我的钱明明白白的扣成功了,怎么订单状态显示还是待支付?
当时我还是很淡定的,并且很快就找到了真凶。
我们知道在软件设计中非常讲究分层的概念,从数据库中取到的数据一般放到DO对象中,这个对象的活动范围也非常受限,并不会直接与上层进行交互。在我们的设计中,一般会把DO对象复制到其对应的实体类中,也就是Entity对象,然后由这个实体类负责对外的交互。
好,问题就出在这个复制的过程中。在将对象属性从DO类复制到Entity类的过程中,我们使用的是Spring提供的BeanUtils类,然而复制过程中,DO中存在主键在Entity中神奇地丢失了!
在说明问题之前,我们先简单看下DO类和Entity类的定义:
public class OrderDO extends BaseDO<Long> {
private String orderId;
}
复制
public abstract class BaseDO<T extends Serializable> {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
protected T id;
}
复制
public class OrderEntity implements Serializable {
private Long id;
private String orderId;
}
复制
属性复制的代码也非常简单:
OrderEntity orderEntity = new OrderEntity();
BeanUtils.copyProperties(orderDO, orderEntity);
复制
那么主键id到底是怎么丢失的呢?代码跟进去看一看,很快定位到了问题所在。
for (int i = 0; i < length; ++i) {
PropertyDescriptor targetPd = targetPds[i];
Method writeMethod = targetPd.getWriteMethod();
if (writeMethod != null && (ignoreList == null || !ignoreList.contains(targetPd.getName()))) {
PropertyDescriptor sourcePd = getPropertyDescriptor(source.getClass(), targetPd.getName());
if (sourcePd != null) {
Method readMethod = sourcePd.getReadMethod();
if (readMethod != null && ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())) {
try {
if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
readMethod.setAccessible(true);
}
Object value = readMethod.invoke(source);
if (value != null) {
if (!Modifier.isPublic(writeMethod.getDeclaringClass().getModifiers())) {
writeMethod.setAccessible(true);
}
writeMethod.invoke(target, value);
}
} catch (Throwable t) {
throw new FatalBeanException("Could not copy property " + targetPd.getName() + " from source to target", t);
}
}
}
}
}
复制
重点看这一行代码,原来在进行属性复制时,Spring的BeanUtils会比较目标属性类型是否是源属性类型的父类或者父接口。
ClassUtils.isAssignable(writeMethod.getParameterTypes()[0], readMethod.getReturnType())
复制
在我们的例子中,writeMethod就是目标属性OrderEntity.id的setter方法,readMethod就是源目标属性OrderDO.id的getter方法。从下图可以看到,OrderEntity.id是Long类型的没有问题,但是OrderDO.id却不是我们想象中的Long类型,而是其对应的泛型Serializable,显然Long并不是Serializable的父类,因此id并不满足进行属性复制的条件,导致复制完成后,OrderEntity.id为null.
在我们的例子中,总结来说就是Spring的BeanUtils无法很好地支持泛型。
第二次尝试
既然Spring的BeanUtils不行,那就试试Apache的吧😆
可是换了Apache的BeanUtils之后,进行属性复制时直接报错了:
org.apache.commons.beanutils.ConversionException: No value specified for 'Date'
at org.apache.commons.beanutils.converters.AbstractConverter.handleMissing(AbstractConverter.java:310)
at org.apache.commons.beanutils.converters.AbstractConverter.convert(AbstractConverter.java:136)
at org.apache.commons.beanutils.converters.ConverterFacade.convert(ConverterFacade.java:60)
at org.apache.commons.beanutils.BeanUtilsBean.convert(BeanUtilsBean.java:1078)
at org.apache.commons.beanutils.BeanUtilsBean.copyProperty(BeanUtilsBean.java:437)
at org.apache.commons.beanutils.BeanUtilsBean.copyProperties(BeanUtilsBean.java:286)
at org.apache.commons.beanutils.BeanUtils.copyProperties(BeanUtils.java:137)
复制
此刻,我的心情是这样的:
第三次尝试
原来,Apache的BeanUtils对日期的支持不是很好,当源对象的日期属性为空时,进行属性复制就会报错。当然要解决这个问题也很简单,Apache提供了扩展接口,只需要注册自己的日期转换器,当日期对象为空时,对目标对象给默认值null就好了,如下:
@Slf4j
public class BeanUtils {
static {
// 注册Date的转换器,当日期对象为空时,对目标对象给默认值null
ConvertUtils.register(new SqlDateConverter(null), Date.class);
}
public static void copyProperties(Object dest, Object orig) {
try {
org.apache.commons.beanutils.BeanUtils.copyProperties(dest, orig);
} catch (Exception e) {
log.error("copy properties error", e);
}
}
}
复制
这么一改之后,所有流程走下来都没有问题了,当然死锁是否还会继续发生还有待进一步的观察。
总结
你看,从第一次出现死锁到现在,没有一步是走得比较顺利的。整个过程总结下来就是:你永远不知道在你前面等待你的是一个什么样的坑。其实这就是程序员的日常,也许下面这幅图最能表达我们的这种感受。
最后,用一句鸡汤文来结尾吧:
你能走多远,取决于你填的坑有多大!