SQL 类型转换错误得到的一些启示

前几天碰到动态 SQL 传 timestamp 参数,运行失败的案例,细思值得拿来一说。

有一张表,叫做 userGroup, 保存的是用户对应各自组的数据。该表上有一个字段 , 其字段类型是 timestamp. 设计这一个字段的目的,是保障数据行记录一致性,而同时还能提高并发。

在 OLTP 应用系统中,我们修改记录的大致过程是这样的:
1) 根据唯一属性字段,找到需要修改的那条记录
2)在此记录上,修改一些属性,用户确认修改正确性
3)点击提交保存,此时数据库方才更改记录

在一个正常的 OLTP 应用系统中,同时会有很多人在用这套系统,所以真实的过程有可能是这样的:
1) 根据唯一属性字段,找到需要修改的那条记录: 当用户 A 查询到了该条记录时,用户 B 也查询到了该条记录
2) 在此记录上,修改一些属性,用户确认修改正确性: 用户 A 对该记录做了很大的修改,所以他花了很长时间做了比对以确保数据修改的正确性。此时 用户 B 也对该记录了修改,而他的修改比较少,仅一两个字段,所以他确认完毕正确性后,准备提交修改
3) 点击提交保存,此时数据库方才更改记录:用户 B 确认完自己修改的正确性后,点击提交保存,此时数据库的数据为用户 B 所希望看到的数据。而在用户 B 提交完修改之后, 用户 A 也提交了自己的修改,此时 A 的数据如果与 B 修改的数据有重合,那么数据库保存的将是 A 的数据,而 B 的数据就会被改写,而当 B 刷新数据的时候,发现自己的改动被重写了,于是又要重复输入一遍。而 A 也在刷新数据,发现重写后,他又重复了一遍输入。所以巧合的场景便反复出现

为了防止此类问题出现,通常会用事务来控制,在需要修改的数据上面几把锁,使得其他用户不能修改正在修改的数据。一旦锁住,别的用户便不能修改此份数据,数据倒是安全了,但别的用户也把时间花在了等待上面。为了提高并发,使得一份数据被锁住的同时,其他人可以等待固定的时间,比如淘票票锁座经常用 45s ,在锁定时间内,数据不可更改。除此之外,还可以给数据加上一分 timestamp. 一旦数据经过修改,timestamp 就自动置位一个增长的新值 , 在别的用户提交更新的时候,会判断当前数据行的 timestamp 与查询时拿到的 timestamp 是否一致。如果不一致说明数据已经被别的用户更改,需要重新同步确认数据的一致性。

create table dbo.UserGroup(
    UserId int identity(1,1)
,   UserName varchar(100) 
,   UserGroup varchar(200)
,   x_timestamp timestamp 
) 


alter procedure dbo.UserGroup_Upsert(
    @UserId int = null
,   @UserName varchar(100)
,   @UserGroup varchar(100)
,   @Xtimestamp timestamp = null
) 
As 
begin 
    if exists(select 1 from UserGroup with(nolock) where UserId = @UserId)
    begin 
        if exists(select 1 from  UserGroup with(nolock) where UserId = @UserId and x_timestamp = @Xtimestamp ) 
        begin 
            update U 
                set UserName = @UserName 
                ,   UserGroup = @UserGroup 
            from UserGroup U 
            where U.UserId = @UserId 
        end 
        else 
        begin 
            raiserror('the data has been edited alread by others',16,1)
            return 
        end
    end 
    else 
    begin 
        insert into UserGroup(UserName,UserGroup) 
            select @UserName, @UserGroup 
    end 
end 

GO

最原始的版本,是将 timestamp 转换成了 Bigint 类型,再拼接成 SQL,来调用修改用户组的存储过程

declare @sql nvarchar(max) 
declare @UserId int = 32 
declare @UserName varchar(100) = 'David.Ciliva'
declare @UserGroup varchar(100) = 'Sales'
declare @Xtimestamp bigint 

select @Xtimestamp = convert(bigint,x_timestamp )
from dbo.UserGroup 
where UserId = @UserId 

set @sql = N' exec dbo.UserGroup_Upsert @UserId = '''+Convert(varchar,@UserId)+''',@UserName ='''+@UserName + ''',@UserGroup = '''+@UserGroup+''',@Xtimestamp = '+ convert(varchar,@Xtimestamp)
exec sp_executesql @stmt = @sql 

select * from UserGroup where UserId = 32 
go 

很长一段时间内,该段脚本运行的很好。直到一次,出现了timestamp 类型转换成 bigint, 出现了溢出。
经过 google, 用 master.dbo.fn_varbintohexstr 转换 timestamp 才避免了此类错误

declare @sql nvarchar(max)  declare @UserId int = 32  declare @UserName varchar(100) = 'David.Ciliva' declare @UserGroup varchar(100) = 'Sales' declare @Xtimestamp nvarchar(max) 

select @Xtimestamp = master.dbo.fn_varbintohexstr(x_timestamp) from dbo.UserGroup  where UserId = @UserId 

set @sql = N' exec dbo.UserGroup_Upsert @UserId = '''+Convert(varchar,@UserId)+''',@UserName ='''+@UserName + ''',@UserGroup = '''+@UserGroup+''',@Xtimestamp = '+ convert(varchar,@Xtimestamp) exec sp_executesql @stmt = @sql 


select * from UserGroup where UserId = 32 


go

实际上,一劳永逸的办法是转成下面的写法,一来可缓存执行计划,避免类型转换的麻烦,提高执行效率,二来防止 SQL 注入。

declare @sql nvarchar(max) 
declare @UserId int = 32 
declare @UserName varchar(100) = 'David.Ciliva'
declare @UserGroup varchar(100) = 'Sales'
declare @Xtimestamp timestamp

select @Xtimestamp =x_timestamp
from dbo.UserGroup 
where UserId = @UserId 

set @sql = N' exec dbo.UserGroup_Upsert @UserId = @UserId,@UserName =@UserName,@UserGroup = @UserGroup,@Xtimestamp = @Xtimestamp '

exec sp_executesql @stmt = @sql 
                ,  @param  = N' @UserId int, @UserName varchar(100), @UserGroup varchar(100), @Xtimestamp timestamp'
                ,   @UserId = @UserId
                ,   @UserName = @UserName 
                ,   @UserGroup = @UserGroup 
                ,   @Xtimestamp = @XtimeStamp 


select * from UserGroup where UserId = 32 

在这个过程中,master.dbo.fn_varbintohexstr 是重点关注的对象。平时我们没有调用系统函数的习惯,大部分人很可能不知道 有这种神奇的函数存在。所以深入的挖一挖,类似的函数,存储过程还有什么神奇的用法。
经过 google , 这类系统函数被称之为 Undocumented function 或者 Undocumented stored procedure, 通俗地讲,就是未归档的系统对象,主要是供微软内部工程师使用,解决一些特殊应用,因为在将来的版本中可能会被淘汰,所以不曝光给大众使用。但从上面的例子来看,确实用途极大。

再讲个未被曝光的函数 , 参考 databasejournal 上的一篇范例:
https://www.databasejournal.com/features/mssql/article.php/3441031/SQL-Server-Undocumented-Stored-Procedures-spMSforeachtable-and-spMSforeachdb.htm
SQL Server Undocumented Stored Procedures sp_MSforeachtable and sp_MSforeachdb

sp_MSforeachtable: 顾名思义,就是对数据库中每一张表做一项指定的操作,比如统计表中的字段。

use home
go 
create table #rowcount (tablename varchar(128), rowcnt int)
exec sp_MSforeachtable 
   'insert into #rowcount select ''?'', count(*) from ?'
select top 50000 * from #rowcount
    order by tablename
drop table #rowcount

如果是统计记录这类操作,用 sys.partitions 的统计信息来得更直观一些,上面的方法仅在于认识 sp_MSForeachTable 这个存储过程

select object_name(object_id) as objname, rows
from sys.partitions with(nolock)
where index_id in (0,1)

sp_MSForeachTable 调用的格式要符合以下规范:

exec @RETURN_VALUE=sp_MSforeachtable @command1, @replacechar, @command2,
@command3, @whereand, @precommand, @postcommand
Where:
@RETURN_VALUE - is the return value which will be set by “sp_MSforeachtable”
@command1 - is the first command to be executed by “sp_MSforeachtable” and is defined as a nvarchar(2000)
@replacechar - is a character in the command string that will be replaced with the table name being processed (default replacechar is a “?”)
@command2 and @command3 are two additional commands that can be run for each table, where @command2 runs after @command1, and @command3 will be run after @command2
@whereand - this parameter can be used to add additional constraints to help identify the rows in the sysobjects table that will be selected, this parameter is also a nvarchar(2000)
@precommand - is a nvarchar(2000) parameter that specifies a command to be run prior to processing any table
@postcommand - is also a nvarchar(2000) field used to identify a command to be run after all commands have been processed against all tables

注意
1 如果不指定每个参数名,则调用时必须按照参数的顺序,传输参数值
2 占位符可以更改, ‘insert into #rowcount select ”?”, count() from ?’ 可以改成 ‘insert into #rowcount select ”&”, count() from &’,’&’
3 @whereand 可以使用 sys.objects 中任意字段的表达式 , 比如
exec sp_MSforeachtable
‘insert into #rowcount select ”&”, count(*) from &’,’&’,null,null,’ and o.name like ”a%” ’
4 @whereand 中,o.name 中的 “o” 代表的是一个别名,这些神秘对象的存在之所以神秘,有些内在的规定是需要我们去挖掘的。可以观察执行计划得到内部的秘密
5 这些神秘函数我们可以通过 google 细查一些应用场景,但官方的解释,因为极其不稳定,不赞成使用。有兴趣可以参考这篇文档的汇总
https://social.technet.microsoft.com/wiki/contents/articles/16975.aspx

猜你喜欢

转载自blog.csdn.net/wujiandao/article/details/80785317
今日推荐