遇到的问题:
线上业务经常出现死锁,查看日志被锁对象是一个应用高频表。
思路:
一开始怀疑是因为高频表,业务操作比较多造成的死锁,确实也有一些功能也是执行时间较长,开启了长事务,但是发现一些对该表的根据id更新的简单操作也会死锁,于是就怀疑表的设计有问题,查看了下表发现该表也有主键索引,于是还怀疑是不是sqlserver或者表的锁策略有问题,于是查看read_committed_snapshot是否开启
SELECT name, is_read_committed_snapshot_on
FROM sys.databases
WHERE name = Databasename;
结果显示开启了
查看主键是否有聚合索引,结果显示也有
SELECT name, type_desc
FROM sys.indexes
WHERE object_id = OBJECT_ID('tablename')
到这里再继续怀疑是数据库的问题,已经不容易继续了,也没有思路,于是决定写几个测试用例,复现一下死锁场景
测试:
为了验证死锁出现的最低阈值,整个测试用例场景是从简单到复杂逐渐递加,数据库语句采用了最简单的根据主键id更新一个字段的语句,然后用for循环进行更新
update table set 字段名称=测试数据 where id = ?
然后采用了三种更新的方式
1:java 使用for循环,里面使用UpdateWrapper进行更新
2:java 使用for循环,里面使用xml更新语句进行更新
3:在xml中使用<foreach>循环更新
测试用例选择了五条,每条测试用例包含45条数据,每次调用测试方法,方法内都循环45次更新
一开始选择两个线程,每个线程循环调用5次测试用例,然后采用结1、2的更新方式,结果,如下图,线程死锁的请求都占了30%,首先耗时有点不正常,然后两个线程就死锁了,这还是只用主键更新一条数据
然后测试了,第三种的更新方法,发现是100%通过 如下图(下图开启了5个线程,2个线程的没有保存),虽然成功了,但这时间明显是不对了,太长了,感觉问题还是没有找清楚
按照上面的测试结果,我直接将测试用例减少为一个,只用两个线程,每个线程只调用一次接口,并且这两次调用的测试用例完全不一样,保证每次update更新的主键都不相同,按照行锁的概念,本应该不会发生死锁,但是结果还是发生了死锁,于是我查询了数据库的死锁事件如下图
锁类型是key,说明是走的索引,那为什么感觉像是索引失效了,看到后面的更新语句我发现了华点,主键是varchar类型,更新的时候传的是nvarchar(4000),是不是数据类型不一样导致索引失效了呢,于是我把更新语句加上了cast进行了一次转换,再进行并发测试,结果如下图
没有死锁了,执行时间明显减少了,看来是索引生效了了,那就是varchar变为nvarchar的原因
解决方法:
在jdbc连接url上添加sendStringParametersAsUnicode=false
原因:默认情况下,java的字符数据是作为Unicode来处理的,所以jdbc驱动连接数据库传输字符数据默认是unicode编码,在sqlserver中支持unicode编码的数据类型是nvarchar、nchar、ntext,而varchar是非unicode,所以之前的条件会转成nvarchar;sendStringParametersAsUnicode=false的意思是指定字符数据的预定义参数作为 ASCII 或多字节字符集 (MBCS) 而不是 Unicode 来发送
参考文献