为什么数据库有时候不能定位阻塞(Blocker)源头的SQL语句 - 潇湘隐者 - 博客园

mikel阅读(487)

来源: 为什么数据库有时候不能定位阻塞(Blocker)源头的SQL语句 – 潇湘隐者 – 博客园

   在SQL Server数据库或OACLE数据库当中,通常一个会话持有某个资源的锁,而另一个会话在请求这个资源,就会出现阻塞(blocking)。这是DBA经常会遇到的情况。当出现SQL语句的阻塞时,很多人想查看阻塞的源头(哪个SQL语句阻塞了哪个SQL),这样方便直观、简洁明了的定位问题。但是很多时候,很多场景,我们通过SQL语句并不能或者说不容易定位到阻塞者(Blocker)的SQL语句,当然我们可以很容易找到被阻塞的SQL语句,以及它在等待的锁资源。下面我们先分析一下SQL Server数据库的这类场景,然后分析一下ORACLE数据库的这类场景。如有不足的地方,敬请指出。

 

在SQL Server当中,我们先准备下面测试环境(测试用的表和数据)。

 
USE Test;
GO
CREATE TABLE Test 
(
    ID        INT ,
    NAME    VARCHAR(12)
);
 
 
INSERT INTO Test
VALUES (1000, 'Kerry');
 
INSERT INTO Test
VALUES(1001, 'Jimmy');

 

场景1:我们构造这样一个简单的场景,例如如下:

 

在会话81中执行下面SQL语句

BEGIN TRAN

    UPDATE Test SET NAME='Tina' WHERE ID=1000;

 

 

在会话72中执行下面SQL语句

SELECT * FROM TEST;

 

 

在另外一个会话窗口执行下面语句,查看阻塞(blocker)者和被阻塞者的SQL语句(这里能够定位到阻塞者(blocker)的SQL语句)。如下所示

SELECT wt.blocking_session_id                    AS BlockingSessesionId
      ,sp.program_name                           AS Blocking_ProgramName
      ,COALESCE(sp.LOGINAME, sp.nt_username)     AS Blocking_HostName
      ,ec1.client_net_address                    AS ClientIpAddress
      ,db.name                                   AS DatabaseName
      ,wt.wait_type                              AS WaitType
      ,ec1.connect_time                          AS BlockingStartTime
      ,wt.WAIT_DURATION_MS/1000                  AS WaitDuration
      ,ec1.session_id                            AS BlockedSessionId
      ,h1.TEXT                                   AS BlockedSQLText
      ,h2.TEXT                                   AS BlockingSQLText
FROM sys.dm_tran_locks  AS tl WITH(NOLOCK)
INNER JOIN sys.databases AS db  WITH(NOLOCK)
  ON db.database_id = tl.resource_database_id
INNER JOIN sys.dm_os_waiting_tasks AS wt  WITH(NOLOCK)
  ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.dm_exec_connections  ec1 WITH(NOLOCK)
  ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections  ec2 WITH(NOLOCK)
  ON ec2.session_id = wt.blocking_session_id
LEFT OUTER JOIN master.dbo.sysprocesses AS sp WITH(NOLOCK)
  ON SP.spid = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

clip_image001

 

但是这个场景是一个非常理想化的场景,实际场景中,可能会话81接下来会去执行其它SQL语句,它并不会一直停留在这个SQL语句上,例如,我们在会话81中执行SELECT GETDATE();这个SQL语句

BEGIN TRAN

 UPDATE Test SET NAME='Tina' WHERE ID=1000;

 SELECT GETDATE();

clip_image002

 

如上所示,此时查到的Blocker者的SQL语句为”SELECT GETDATE();”, 而这个SQL其实和被阻塞的SQL没有半毛关系。即使使用sp_WhoIsActive这样专业的SQL亦是如此。

clip_image003

 

当然我们可以查看其等待的锁对象信息,这也是我们所能追踪、捕获的。如下所示:

<Database name="Test">
  <Locks>
    <Lock request_mode="S" request_status="GRANT" request_count="1" />
  </Locks>
  <Objects>
    <Object name="Test" schema_name="dbo">
      <Locks>
        <Lock resource_type="OBJECT" request_mode="IS" request_status="GRANT" request_count="1" />
        <Lock resource_type="PAGE" page_type="*" request_mode="IS" request_status="GRANT" request_count="1" />
        <Lock resource_type="RID" page_type="*" request_mode="S" request_status="WAIT" request_count="1" />
      </Locks>
    </Object>
  </Objects>
</Database>

clip_image004

 

这种场景,如果只是某个会话发出的即席查询,那么你几乎已经很难捕获到阻塞的源头UPDATE Test SET NAME=’Tina’ WHERE ID=1000这个SQL语句了。除非你结合其它一些手段,逆向推断。

 

场景2:上面查找SQL阻塞的SQL语句,有时候只能定位到某一个存储过程或一大段即席查询SQL。

 

例如,下面一个构造的存储过程,一个用户正在一个会话当中执行它,

CREATE PROCEDURE PRC_TEST
AS
BEGIN

    BEGIN TRAN TR1

    UPDATE Test SET NAME='YourName' WHERE ID=1000;

    SELECT * FROM sys.sysprocesses WHERE spid=@@SPID;

    WAITFOR DELAY '00:00:20';

    COMMIT TRAN TR1;

END
GO

 

另外一个用户在另外一个会话执行下面查询SQL语句


SELECT * FROM TEST;

 

查看阻塞的历史记录

clip_image005

 

你会看到捕获的是整个存储过程,当然这个测试案例很容易知道是那个SQL语句阻塞了,实际的存储过程可能业务很复杂,SQL语句也非常多,你想从一个存储过程里面找到阻塞者(Blocker)的SQL语句其实是非常麻烦的。需要你仔细甄别,当存储过程的业务逻辑复杂,SQL语句非常多时,这是一个头痛的事情

其实遇到这些场景,我们大可不必一定要查看阻塞这(Blocker)的具体SQL,我们只需要查看被阻塞者,等待的锁对象资源的相关信息即可,你可以大致判断到底是一个什么类型的SQL导致了这类阻塞。

 

 

那么我们接下来看看ORACLE数据库场景吧。我们先准备一个测试环境(测试表和相关数据)

 

CREATE TABLE "TEST"."TEST"
(    "ID" NUMBER,
    "NAME" VARCHAR2(12)
);

INSERT INTO TEST
SELECT 1001, 'jimmy' FROM DUAL UNION ALL
SELECT 1002, 'Kerry' FROM DUAL;

COMMIT;

 

 

接下来我们在会话窗口一执行下面SQL:

[oracle@DB-Server ~]$ sqlplus test/test

SQL*Plus: Release 11.2.0.1.0 Production on Tue Aug 30 10:16:43 2016

Copyright (c) 1982, 2009, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='KKK' WHERE ID =1001;

1 row updated.

SQL>

 

在另外一个会话窗口二执行下面SQL

[oracle@DB-Server ~]$ sqlplus test/test

SQL*Plus: Release 11.2.0.1.0 Production on Tue Aug 30 10:17:22 2016

Copyright (c) 1982, 2009, Oracle.  All rights reserved.


Connected to:
Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='Ken' WHERE ID =1001;

 

然后我们在第三个窗口执行下面SQL语句,查看阻塞和被阻塞的SQL语句

SELECT dba_objects.object_name,
       locks_t.row#,
       locks_t.blocked_secs,
       locks_t.blocker_text,
       locks_t.blocked_text,
       locks_t.blocked_sql_text,
       locks_t.blocking_sql_text
  FROM (SELECT /*+ NO_MERGE */
               blocking_lock_session.username||'@'||blocking_lock_session.machine||'(SID='||blocking_lock_session.sid||') ['||
               blocking_lock_session.program||'/PID='||blocking_lock_session.process||']' as blocker_text,
               blocked_lock_session.username||'@'||blocked_lock_session.machine|| '(SID='||blocked_lock_session.sid||') ['||
               blocked_lock_session.program||'/PID='||blocked_lock_session.process||']' as blocked_text,
               blocked_lock_session.row_wait_obj#,
               blocked_lock_session.row_wait_file#,
               blocked_lock_session.row_wait_block#,
               blocked_lock_session.row_wait_row#,
               DBMS_ROWID.ROWID_CREATE (1,
                  blocked_lock_session.row_wait_obj#,
                  blocked_lock_session.row_wait_file#,
                  blocked_lock_session.row_wait_block#,
                  blocked_lock_session.row_wait_row#) row#,
               blocked_lock_session.seconds_in_wait blocked_secs,
               blocked_sql.sql_text blocked_sql_text,
               blocking_sql.sql_text blocking_sql_text
          FROM v$lock blocking_lock,
               v$session blocking_lock_session,
               v$lock blocked_lock,
               v$session blocked_lock_session,
               v$sql blocked_sql,
               v$sql blocking_sql
         WHERE blocking_lock.block = 1
           AND blocking_lock.id1 = blocked_lock.id1
           AND blocking_lock.id2 = blocked_lock.id2
           AND blocked_lock.request > 0
           AND blocking_lock.sid = blocking_lock_session.sid
           AND blocked_lock.sid = blocked_lock_session.sid
           AND blocked_lock_session.sql_id = blocked_sql.sql_id
           AND blocked_lock_session.sql_child_number = blocked_sql.child_number
           AND blocking_lock_session.PREV_SQL_ADDR(+)  =blocking_sql.ADDRESS
       ) locks_t,
       dba_objects
 WHERE locks_t.row_wait_obj# = dba_objects.object_id
ORDER BY locks_t.blocked_secs;

clip_image006

 

如果我们在会话窗口1,再执行一个语句,如下所示

 

SQL> show user;
USER is "TEST"
SQL> UPDATE TEST SET NAME='KKK' WHERE ID =1001;

1 row updated.

SQL> select * from dual;

D
-
X

 

此时捕获到的是select * from dual; 这个SQL跟被阻塞的SQL没有任何关系,当然如果你继续在会话窗口执行其它SQL语句,捕获的都是不相关的SQL语句,已经没有任何意义

clip_image007

 

出现这个问题,是因为当一个会话正在执行某个SQL语句,那么v$session视图中的SQL_ID记录的是正在执行SQL的SQL_ID,当会话空闲或执行其它SQL语句后,SQL_ID就会变化,PRE_SQL_ID记录上一个执行完的SQL的SQL_ID值,PREV_SQL_ADDR也是如此。如下英文所述 :

 

According to the Reference Manual entry for V$SESSION the SQL_ID column represents the current SQL statement being executed by a session.  If the session is idle there is no current SQL statement.  Also if a session performs an update then performs a query the SQL_ID would reflect the query and not the update which is the statement that is blocking.  There is in fact no query that is guaranteed to find the blocking SQL.  Unless the blocking statement is the current statement all you can find for sure I the blocking session

 

如果你不用SQL*Plus,使用PL/SQL Developer这个工具,你会看到BLOCKING_SQL_TEST永远都是begin sys.dbms_output.get_line(line => :line, status => :status); end; 这个是因为PL/SQL Developer在执行完SQL后,会调用其它SQL语句,当然SQL Developer不会有这样的问题。

 

所以综上述,想要找到阻塞的源头SQL语句,只用SQL查询,其实在很多场景是不太现实的,所以很多SQL语句都只给出阻塞者的会话信息或锁定对象信息。如下所示

 

会话ID为8的会话执行下面SQL

UPDATE TEST SET NAME='TEST' WHERE ID=1001;

 

会话ID为137的会话执行下面SQL

UPDATE TEST SET NAME='TES1' WHERE ID=1001;

然后我们使用get_locked_objects_rpt.sql查看被阻塞的SQL,以及锁定相关对象的信息(get_locked_objects_rpt.sql请参考get_locked_objects_rpt.sql

SQL> @get_locked_objects_rpt.sql
Enter value for 1: 6
old  42:    AND locks_t.blocked_secs > &1
new  42:    AND locks_t.blocked_secs > 6
========= $Revision: 1.4 $ ($Date: 2013/09/16 13:15:22 $) ===========
Locked object : TEST
Locked row#   : AAASEkAAEAAAADVAAA
Blocked for   : 19 seconds
Blocker info. : TEST@GFG1\GET253194(SID=8) [plsqldev.exe/PID=17988:14616]
Blocked info. : TEST@get253194(SID=137) [SQL Developer/PID=17780]
Blocked SQL   : UPDATE TEST SET NAME='TES1' WHERE ID=1001
Found 1 blocked session(s).
Disconnected from Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production
With the Partitioning, OLAP, Data Mining and Real Application Testing options
[oracle@DB-Server ~]$

clip_image008

然后我通过上面的Locked Object知道被锁定的对象为Test表的ROWID为AAASRCAAEAAAADVAAA的记录,如下所示

clip_image009

 

 

SQL Server中SELECT会真的阻塞SELECT吗? - 潇湘隐者 - 博客园

mikel阅读(474)

来源: SQL Server中SELECT会真的阻塞SELECT吗? – 潇湘隐者 – 博客园

SQL Server中,我们知道一个SELECT语句执行过程中只会申请一些意向共享锁(IS) 与共享锁(S), 例如我使用SQL Profile跟踪会话86执行SELECT * FROM dbo.TEST WHERE OBJECT_ID =1 这个查询语句,其申请、释放的锁资源的过程如下所示:

而且从最常见的锁模式的兼容性表,我们可以看到IS锁与S锁都是兼容的,也就是说SELECT查询是不会阻塞SELECT查询的。

现有的授权模式
请求的模式 IS S U IX SIX X
意向共享 (IS)
共享 (S)
更新 (U)
意向排他 (IX)
意向排他共享 (SIX)
排他 (X)

但是在某些特殊场景。你会看到SELECT语句居然“阻塞”SELECT操作,那么SQL Server中SELECT会真的阻塞SELECT操作吗?我们先构造测试的案例场景,那么先准备测试数据吧

CREATE TABLE TEST (OBJECT_ID  INT, NAME VARCHAR(8)); CREATE INDEX PK_TEST ON TEST(OBJECT_ID)   DECLARE @Index INT =0; WHILE @Index < 20BEGIN    INSERT INTO TEST    SELECT @Index, ‘kerry’;        SET @Index = @Index +1;END

在会话窗口A中,执行下面SQL语句,模拟一个UPDATE语句正在执行

 BEGIN TRANSACTION     UPDATE dbo.TEST SET NAME=’Kerry’ WHERE OBJECT_ID=1;    –ROLLBACK;

会话窗口B中,执行下面的SQL语句

SELECT * FROM dbo.TEST WHERE OBJECT_ID=1

会话窗口C中,执行下面的SQL语句

SELECT * FROM dbo.TEST WHERE OBJECT_ID=1

我实验的场景下,会话窗口A的会话ID为85,会话窗口B的会话ID为90,会话窗口C的会话ID为87,如下所示

如下所示,你会看到SELECT语句“阻塞”了SELECT语句,即会话90“阻塞”了会话87, 它们的等待事件都为LCK_M_S,也就是说它们都在等待获取共享锁,也许你会置疑这个SQL是否有问题,那么我们使用SP_WHO来查看,你会发现也是如此,如下所示:

如下所示,我们会发现会话ID为90 、87的会话都在等待类型为RID,Resource为1:24171:1的共享锁

其实应该说,会话87、90都在等待RID对象的共享锁,我们知道共享锁与意向共享锁都是兼容的,所以SELECT是不会阻塞SELECT的,那么又怎么解释这个现象呢?在宋大神的指点下,粗略的翻了Database System Implementaion这本书(很多原理性知识,看起来相当吃力)。里面介绍了在锁表(lock table)以及Element Info、Handling Lock Requests、Handling Unlocks等概念,有一个有意思的图所示,

在锁表(lock table)里,elements info里的锁的申请是在一个类似队列的结构。先进先出机制,所以当会话90先进入队列,它在等待共享锁(S), 会话87也进入队列等待共享锁(S),而且它在会话90的后面(即会话90这个elements info后面的Next指针指向会话87会话的事务),由于两个会话都被阻塞,这两个会话的Wait字段都是Yes,由于内部某些机制,会话87显示阻塞它的会话为90(这个是我个人臆测,实际具体原因有待考究),实质阻塞的源头还是会话85. 当会话85释放排它锁(X)后,会话队列根据下面几个原则来处理解锁(Handling Unlocks):

1: First-come-first-served: Grant the lock request that has been waiting the longest. This strategy guarantees no starvation, the situation where a transaction can wait forever for a lock

先来先服务(队列的原则):授予锁等待时间最长的锁请求,这种策略保证不会饿死(翻译感觉不贴切),即一个事务不会永远等待锁的情况。

  1. Priority to shared locks: First grant all the shared locks waiting. Then,grant one update lock, if there are any waiting. Only grant an exclusivelock if no others are waiting. This strategy can allow starvation, if atransaction is waiting for a U or X lock.

共享锁优先,首先授予所有等待共享锁(S),然后授予其中一个更新锁(U),如果有其它类型等待,只有在没有其它锁等待时,才授予排它锁、这一策略允许等待更新锁或排它锁的事务饿死(结束)

  1. Priority to upgrading: If there is a transaction with a U lock waiting toupgrade it to an X lock, grant that first. Otherwise, follow one of theother strategies mentioned.

锁升级优先,如果有一个持有共享锁(U)等待升级Wie排他锁(X),那么先授予它排它锁,否则采用前面已经提到的策略中的一个。

按照这些原则,当会话85释放了排它锁(X)后,调度器(Scheduler)应该会根据先后顺序依次授予会话90、87共享锁(S),两者的阻塞会几乎同时消失。 这个可以也可以通过实验进行一个大概的推断, 在上面实验中,你可以手工取消90会话的查询操作,然后再查看阻塞情况,就会发现会话87被85阻塞了。这个阻塞的源头就变成了85,而不是90了。

PS:上面是个人结合一些知识和理解,做的一些肤浅的判断与分析,如果不对的地方,敬请指正!

参考资料:

Database System Implementaion

MS SQL 日常维护管理常用脚本(二) - 潇湘隐者 - 博客园

mikel阅读(545)

来源: MS SQL 日常维护管理常用脚本(二) – 潇湘隐者 – 博客园

监控数据库运行

下面是整理、收集监控数据库运行的一些常用脚本,也是MS SQL 日常维护管理常用脚本(一)的续集,欢迎大家补充、提意见。

 

查看数据库登录名信息

Code Snippet
  1. SELECT name                                AS LoginName ,
  2.        dbname                              AS DefaultDB ,
  3.        createdate                          AS CreateDate,
  4.        updatedate                          AS UpdateDate,
  5.        language                            AS Language  ,
  6.        CASE WHEN isntname = 1 THEN ‘NT USER’
  7.            ELSE ‘SQL USER’    END          AS UserType
  8. FROM syslogins;

 

查看数据库用户信息

SELECT * FROM sysusers;

查看用户拥有的服务器角色
 

方法1: 用SSMS管理工具查看

方法2: 脚本查询


查看用户角色
  1. SELECT name            ,
  2.        CASE WHEN sysadmin     = 1       THEN ‘yes’     ELSE ” END AS IsSysadmin        ,
  3.        CASE WHEN dbcreator    = 1       THEN ‘yes’     ELSE ” END AS IsDbCreate        ,
  4.        CASE WHEN securityadmin= 1       THEN ‘yes’     ELSE ” END AS IsSecurityadmin   ,
  5.        CASE WHEN bulkadmin    = 1       THEN ‘yes’     ELSE ” END AS IsBulkadmin       ,
  6.        CASE WHEN diskadmin    = 1       THEN ‘yes’     ELSE ” END AS IsDiskadmin       ,
  7.        CASE WHEN processadmin = 1       THEN ‘yes’     ELSE ” END AS IsProcessadmin    ,
  8.        CASE WHEN serveradmin  = 1       THEN ‘yes’     ELSE ” END AS IsServeradmin     ,
  9.        CASE WHEN setupadmin   = 1       THEN ‘yes’     ELSE ” END AS IsSetupadmin
  10. FROM syslogins
  11. –WHERE NAME=’loginname’

查看最大工作线程数

Code Snippet
  1. SELECT  max_workers_count
  2.   FROM  sys.dm_os_sys_info

 

查看当前用户进程的会话ID

 

SELECT @@SPID

 

查询当前会话使用哪种协议

Code Snippet
  1. SELECT net_transport
  2. FROM   sys.dm_exec_connections
  3. WHERE session_id = @@SPID;

查看当前连接的会话信息

–进程号1–50是SQL Server系统内部用的

SELECT * FROM sys.dm_exec_sessions WHERE session_id >=51

–查看某台机器的连接会话信息

SELECT * FROM sys.dm_exec_sessions WHERE session_id >=51 AND host_name='PO130018801'

–查看某个登录名的连接会话信息

SELECT * FROM sys.dm_exec_sessions WHERE session_id >=51 AND login_name='username'

–查看活动的连接会话信息

SELECT * FROM sys.dm_exec_sessions WITH(NOWAIT) WHERE session_id >=51 AND status ='running'

–查找连接到服务器的用户并返回每个用户的会话数

SELECT  login_name ,
        COUNT(session_id) AS session_count
FROM    sys.dm_exec_sessions
GROUP BY login_name ;
查看正在执行的SQL语句

方法1: 选择数据库实例,单击右键,选择”活动监视器“,监控/查看正在执行的SQL

方法2: profile去跟踪,比较耗费资源。

 

方法3:


Code Snippet
  1. SELECT[Spid] = session_Id ,
  2.       ecid ,
  3.       [Database] = DB_NAME(sp.dbid) ,
  4.       [User] = nt_username ,
  5.       [Status] = er.status ,
  6.       [Wait] = wait_type ,
  7.       [Individual Query] = SUBSTRING(qt.text, er.statement_start_offset / 2,
  8.                                      ( CASE WHEN er.statement_end_offset = -1
  9.                                             THEN LEN(CONVERT(NVARCHAR(MAX), qt.text))
  10.                                                  * 2
  11.                                             ELSE er.statement_end_offset
  12.                                        END – er.statement_start_offset ) / 2) ,
  13.       [Parent Query] = qt.text ,
  14.       Program = program_name ,
  15.       Hostname ,
  16.       nt_domain ,
  17.       start_time
  18.   FROMsys.dm_exec_requests er
  19.       INNER JOIN sys.sysprocesses sp ON er.session_id = sp.spid
  20.       CROSS APPLY sys.dm_exec_sql_text(er.sql_handle) AS qt
  21.  WHEREsession_Id >= 51

方法4:

Code Snippet
  1. SELECT   m.session_id ,
  2.          m.start_time ,
  3.          m.command    ,
  4.          m.wait_type  ,
  5.          m.cpu_time   ,
  6.         CAST(s.text AS VARCHAR(1000)) AS sqlText
  7.  FROMmaster.sys.dm_exec_requests m WITH ( NOLOCK )
  8.      CROSS APPLY fn_get_sql(m.sql_handle) s
  9. SELECT  r.session_id,
  10.         r.start_time      ,
  11.         r.command         ,
  12.         r.wait_type       ,
  13.         r.cpu_time        ,
  14.         s.text
  15.  FROMsys.dm_exec_requests r
  16.      CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) s

–查看某个会话ID正在执行的SQL

Code Snippet
  1. SELECT  m.session_id ,
  2.         m.start_time ,
  3.         m.command    ,
  4.         m.wait_type  ,
  5.         m.cpu_time   ,
  6.       CAST(s.text AS VARCHAR(1000)) AS sqlText
  7.   FROMmaster.sys.dm_exec_requests m WITH ( NOLOCK )
  8.       CROSS APPLY fn_get_sql(m.sql_handle) s
  9.  WHEREm.session_id = 342
  10.     SELECT   r.session_id      ,
  11.              r.start_time      ,
  12.              r.command         ,
  13.              r.wait_type       ,
  14.              r.cpu_time        ,
  15.              s.text
  16.   FROM sys.dm_exec_requests r
  17.       CROSS APPLY sys.dm_exec_sql_text(r.sql_handle) s
  18. WHERE r.seesion_id =342

查看SQL SERVER进程执行的语句

Code Snippet
  1. USE master
  2. DECLARE @spid INT ;
  3. DECLARE @sql_handle BINARY(20) ;
  4. SET @spid = 56
  5.   SELECT@sql_handle = sql_handle
  6.     FROMsysprocesses AS A WITH ( NOLOCK )
  7.    WHEREspid = @spid ;
  8.   SELECTtext
  9.     FROM::fn_get_sql(@sql_handle) ;

查找TOP N语句

按平均 CPU 时间返回排名前十个的查询的相关信息。此示例将根据查询的查询哈希对查询进行聚合,以便按照查询的累积资源消耗来分组在逻辑上等效的查询。
–注意:SQL 2005 某些版本,没有sys.dm_exec_query_stats系统动态视图没有query_hash视图。

Code Snippet
  1. USE DBNAME;
  2. GO
  3. SELECT TOP 10 query_stats.query_hash             AS “Query Hash”,
  4.     SUM(query_stats.total_worker_time) /
  5.     SUM(query_stats.execution_count)             AS “Avg CPU Time”,
  6.     MIN(query_stats.statement_text)              AS “Statement Text”
  7. FROM
  8.     (SELECT QS.*,
  9.     SUBSTRING(ST.text,(QS.statement_start_offset/2) + 1,
  10.     ((CASE statement_end_offset
  11.         WHEN -1 THEN DATALENGTH(st.text)
  12.         ELSE QS.statement_end_offset END
  13.             – QS.statement_start_offset)/2) + 1) AS statement_text
  14.      FROM sys.dm_exec_query_stats AS QS
  15.      CROSS APPLY sys.dm_exec_sql_text(QS.sql_handle) as ST) as query_stats
  16. GROUP BY query_stats.query_hash
  17. ORDER BY 2 DESC;
  18. GO
查看会话阻塞/死锁信息
 

方法1:查看那个引起阻塞,查看blk不为0的记录,如果存在阻塞进程,则是该阻塞进程的会话 ID。否则该列为零。 

EXEC sp_who active

方法2:查看那个引起阻塞,查看字段BlkBy,这个能够得到比sp_who更多的信息。

EXEC sp_who2 active

方法3:sp_lock 系统存储过程,报告有关锁的信息,但是不方便定位问题

方法4:sp_who_lock存储过程

Code Snippet
  1. USE master;
  2. GO
  3. SET ANSI_NULLS ON;
  4. GO
  5. SET QUOTED_IDENTIFIER ON;
  6. GO
  7. IF EXISTS(SELECT 1 FROM sysobjects WHERE id=OBJECT_ID(N’sp_who_lock’)
  8.                                        AND OBJECTPROPERTY(id, ‘IsProcedure’) =1)
  9.     DROP PROCEDURE sp_who_lock;
  10. GO
  11. –==================================================================================================
  12. —            ProcedureName             :            sp_who_lock
  13. —            Author                    :            作者不详,出自网络
  14. —            CreateDate                :            2013-05-13
  15. —            Description               :            查看阻塞和死锁信息
  16. /**************************************************************************************************
  17.         Parameters                    :              参数说明
  18. ***************************************************************************************************
  19.                                                      无参存储过程
  20. ***************************************************************************************************
  21.         Modified Date            Modified User     Version                 Modified Reason
  22. ***************************************************************************************************
  23.        2013-06-03                    Kerry        V01.00.01 调整存储过程格式,代码部分修改以及增加注释信息
  24. ***************************************************************************************************/
  25. –=================================================================================================
  26.   CREATE PROCEDURE sp_who_lock
  27.     AS
  28. BEGIN
  29. DECLARE @spid                            INT;
  30. DECLARE @block                           INT;
  31. DECLARE @RowCount                        INT;
  32. DECLARE @RowIndex                        INT;
  33. –创建临时表,保持被阻塞或正阻塞其他SQL的SQL语句信息
  34. CREATE TABLE #tmp_lock_who
  35. (
  36.       id    INT IDENTITY(1, 1) ,
  37.       spid    SMALLINT ,
  38.       block SMALLINT
  39. )
  40. IF @@ERROR<>0 RETURN @@ERROR;
  41. INSERT INTO #tmp_lock_who
  42.         (
  43.           spid ,
  44.           block
  45.         )
  46.         SELECT  0 ,
  47.                 blocked
  48.         FROM    ( SELECT    *
  49.                   FROM      sysprocesses
  50.                   WHERE     blocked > 0
  51.                 ) a
  52.         WHERE   NOT EXISTS( SELECT *
  53.                              FROM   ( SELECT    *
  54.                                       FROM      sysprocesses
  55.                                       WHERE     blocked > 0
  56.                                     ) b
  57.                              WHERE  a.blocked = spid )
  58.         UNION
  59.         SELECT  spid ,
  60.                 blocked
  61.         FROM    sysprocesses
  62.         WHERE   blocked > 0;
  63. IF @@ERROR<>0 RETURN @@ERROR;
  64. — 找到临时表的记录数
  65.   SELECT@RowCount = COUNT(1) ,
  66.         @RowIndex = 1
  67.     FROM#tmp_lock_who
  68. IF @@ERROR<>0 RETURN @@ERROR;
  69.   IF@RowCount=0
  70.   SELECT  N’现在没有阻塞和死锁信息’ AS MESSAGE;
  71. — — 循环开始
  72. WHILE @RowIndex <= @RowCount
  73. BEGIN
  74.     — 取第一条记录
  75.     SELECT     @spid    = spid,
  76.                @block   = block
  77.     FROM #tmp_lock_who
  78.     WHERE Id = @RowIndex
  79.     IF @spid = 0
  80.         SELECT N’引起数据库死锁的是: ‘ + CAST(@block AS VARCHAR(10))
  81.             +  N’进程号,其执行的SQL语法如下’;
  82.     ELSE
  83.         SELECT N’进程号SPID:’ + CAST(@spid AS VARCHAR(10)) + N’被进程号SPID:’
  84.             +  CAST(@block AS VARCHAR(10)) +’阻塞,其当前进程执行的SQL语法如下’;
  85.     DBCC INPUTBUFFER(@block )
  86.     SET @RowIndex = @RowIndex + 1;
  87. END;
  88.     DROP TABLE #tmp_lock_who;
  89.     RETURN 0;
  90. END

方法5:右键服务器-选择“活动和监视器”,查看进程选项。注意“任务状态”字段。

方法6:右键服务名称-选择报表-标准报表-活动-所有正在阻塞的事务。

小结:总结之后,才发现居然有这么多方法,MGD,系统的整理、梳理知识点是非常有必要的,你能更全面、深入的了解。


查看内存状态

dbcc memorystatus

具体如何分析,请查看官方文档http://support.microsoft.com/kb/907877/zh-cn


查看脚本执行时间

方法1: 查看SSMS管理器,查询窗口右下角
方法2:

Code Snippet
  1. DECLARE @exectime DATETIME
  2.   SELECT@exectime = GETDATE()
  3. –SQL 语句
  4. PRINT N’SQL执行耗时:’ + CONVERT(VARCHAR(10), DATEDIFF(ms, @exectime, GETDATE()))

方法3:

SET STATISTICS TIME ON

–SQL 语句

 

查看进程正在执行的SQL语句 

dbcc inputbuffer ()

查看那些表缺少索引 

 

下面语句功能强大,执行结果受统计信息的影响

Code Snippet
  1. SELECT sys.objects.name table_name,
  2.        mid.statement full_name,
  3.     (migs.avg_total_user_cost * migs.avg_user_impact) *(migs.user_seeks + migs.user_scans) AS Impact,
  4.     migs.avg_user_impact *(migs.user_seeks + migs.user_scans) Avg_Estimated_Impact,
  5.     ‘CREATE NONCLUSTERED INDEX IDX_’ + sys.objects.name + ‘_N ON ‘
  6.         + sys.objects.name COLLATE DATABASE_DEFAULT
  7.         + ‘ ( ‘ + IsNull(mid.equality_columns, ”)
  8.         + CASE WHEN mid.inequality_columns IS NULL
  9.                 THEN ”
  10.             ELSE
  11.                 CASE WHEN mid.equality_columns IS NULL
  12.                     THEN ”
  13.                 ELSE ‘,’
  14.                 END + mid.inequality_columns
  15.             END + ‘ ) ‘
  16.         + CASE WHEN mid.included_columns IS NULL
  17.                 THEN ”
  18.             ELSE ‘INCLUDE (‘ + mid.included_columns + ‘)’ END
  19.         + ‘;’ AS CreateIndexStatement,
  20.     mid.equality_columns,
  21.     mid.inequality_columns,
  22.     mid.included_columns
  23. FROM sys.dm_db_missing_index_group_stats AS migs
  24.     INNER JOIN sys.dm_db_missing_index_groups AS mig ON migs.group_handle = mig.index_group_handle
  25.     INNER JOIN sys.dm_db_missing_index_details AS mid ON mig.index_handle = mid.index_handle
  26.         AND mid.database_id = DB_ID()
  27.     INNER JOIN sys.objects WITH (nolock) ON mid.OBJECT_ID = sys.objects.OBJECT_ID
  28. WHERE (migs.group_handle IN
  29.         (
  30.             SELECT TOP (500) group_handle
  31.             FROM sys.dm_db_missing_index_group_stats WITH (nolock)
  32.             ORDER BY (avg_total_user_cost * avg_user_impact) *(user_seeks + user_scans) DESC))
  33.     AND OBJECTPROPERTY(sys.objects.OBJECT_ID, ‘isusertable’)=1
  34. –ORDER BY [Impact] DESC, [full_name] DESC
  35. ORDER BY [table_name], [Impact] desc

查看应该被移除的索引

 

查看那些多余的、应该被移除的索引

SQL 1:

Code Snippet
  1. SELECT OBJECTNAME = OBJECT_NAME(I.OBJECT_ID),
  2.     INDEXNAME = I.NAME,
  3.     I.INDEX_ID
  4. FROM SYS.INDEXES I
  5.     JOIN SYS.OBJECTS O ON I.OBJECT_ID = O.OBJECT_ID
  6. WHERE OBJECTPROPERTY(O.OBJECT_ID,’IsUserTable’) = 1
  7.     AND I.INDEX_ID NOT IN(
  8.                             SELECT S.INDEX_ID
  9.                             FROM SYS.DM_DB_INDEX_USAGE_STATS S
  10.                             WHERE S.OBJECT_ID = I.OBJECT_ID
  11.                                 AND I.INDEX_ID = S.INDEX_ID
  12.                                 AND DATABASE_ID = DB_ID())
  13. ORDER BY OBJECTNAME, I.INDEX_ID, INDEXNAME ASC

SQL 2: 分析下面语句,移除那些没有必要的索引

Code Snippet
  1. SELECT DB_NAME(database_id)         AS  N’DataBaseName’  ,
  2.        OBJECT_NAME(U.object_id)     AS  N’Table_Name’    ,
  3.        I.name                       AS  N’Index_Name’    ,
  4.        user_seeks                   AS  N’用户索引查找次数’,
  5.        user_scans                   AS  N’用户索引扫描次数’,
  6.        last_user_seek               AS  N’最后查找时间’   ,
  7.        last_user_scan               AS  N’最后扫描时间’   ,
  8.        rows                         AS  N’表中的行数’
  9. FROM sys.dm_db_index_usage_stats AS U
  10.    INNER JOIN sys.indexes I ON U.index_id= I.index_idAND U.object_id= I.object_id
  11.   INNER JOIN sysindexesT ON I.object_id = T.id
  12. WHERE database_id= DB_ID(‘DbName’)
  13. AND OBJECT_NAME(U.object_id)=’TableName’
  14. ORDER BY user_seeks, user_scans, object_name(U.object_id);

基于SqlSugar的开发框架循序渐进介绍(2)-- 基于中间表的查询处理 - 伍华聪 - 博客园

mikel阅读(590)

来源: 基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理 – 伍华聪 – 博客园

在前面介绍的SQLSugar的相关查询处理操作中,我们主要以单表的方式生成相关的实体类,并在查询的时候,对单表的字段进行条件的对比处理,从而返回对应的数据记录。本篇随笔介绍在一些外键或者中间表的处理中,如何遍历查询并获得所需的记录操作。

1、回顾单表的操作查询

我在《基于SqlSugar的开发框架的循序渐进介绍(1)–框架基础类的设计和使用》中介绍过的Customer表信息,就是一个单表的处理。

例如,我们对于一个简单的客户信息表,如下所示。

生成对应的实体对象CustomerInfo外,同时生成 CustomerPagedDto  的分页查询条件对象。

在继承基类后

复制代码
/// <summary>
/// 应用层服务接口实现
/// </summary>
public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
{
       ....
}
复制代码

并重写 CreateFilteredQueryAsync 函数,从而实现了条件的精确查询处理。

复制代码
    /// <summary>
    /// 应用层服务接口实现
    /// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
    {
        /// <summary>
        /// 自定义条件处理
        /// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<CustomerInfo> CreateFilteredQueryAsync(CustomerPagedDto input)
        {
            var query = base.CreateFilteredQueryAsync(input);

            query = query
                .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID
                .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals
                                                                                             //年龄区间查询
                .WhereIF(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value)
                .WhereIF(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value)

                //创建日期区间查询
                .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >= input.CreateTimeStart.Value)
                .WhereIF(input.CreateTimeEnd.HasValue, s => s.CreateTime <= input.CreateTimeEnd.Value)
                ;

            return query;
        }
复制代码

在表的对应实体信息没有其他表关联的时候,我们直接通过SQLSugar的基础接口返回对象列表即可。

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

复制代码
        /// <summary>
        /// 根据条件获取列表
        /// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>> GetListAsync(TGetListInput input)
        {
            var query = CreateFilteredQueryAsync(input);
            var totalCount = await query.CountAsync();

            query = ApplySorting(query, input);
            query = ApplyPaging(query, input);

            var list = await query.ToListAsync();

            return new PagedResultDto<TEntity>(
               totalCount,
               list
           );
        }
复制代码

也就是说只要继承了 CustomerService ,我们默认调用基类的 GetListAsync 就可以返回对应的列表记录了。

如在Web API的控制器中调用获取记录返回,调用处理的代码如下所示。

复制代码
        /// <summary>
        /// 获取所有记录
        /// </summary>
        [HttpGet]
        [Route("all")]
        [HttpGet]public virtual async Task<ListResultDto<TEntity>> GetAllAsync()
        {
            //检查用户是否有权限,否则抛出MyDenyAccessException异常
            base.CheckAuthorized(AuthorizeKey.ListKey);

            return await _service.GetAllAsync();
        }
复制代码

而对于Winform的调用,我们这里首先利用代码生成工具生成对应的界面和代码

查看其调用的界面代码

而其中GetData中的函数部分内容如下所示。

复制代码
        /// <summary>
        /// 获取数据
        /// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<CustomerInfo>> GetData()
        {
            CustomerPagedDto pagerDto = null;
            if (advanceCondition != null)
            {
                //如果有高级查询,那么根据输入信息构建查询条件
                pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo);
                pagerDto = dlg.GetPagedResult(pagerDto);
            }
            else
            {
                //构建分页的条件和查询条件
                pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo)
                {
                    //添加所需条件
                    Name = this.txtName.Text.Trim(),
                };

                //日期和数值范围定义
                //年龄,需在CustomerPagedDto中添加 int? 类型字段AgeStart和AgeEnd
                var Age = new ValueRange<int?>(this.txtAge1.Text, this.txtAge2.Text); //数值类型
                pagerDto.AgeStart = Age.Start;
                pagerDto.AgeEnd = Age.End;

                //创建时间,需在CustomerPagedDto中添加 DateTime? 类型字段CreationTimeStart和CreationTimeEnd
                var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型
                pagerDto.CreateTimeStart = CreationTime.Start;
                pagerDto.CreateTimeEnd = CreationTime.End;
            }

            var result = await BLLFactory<CustomerService>.Instance.GetListAsync(pagerDto);
            return result;
        }
复制代码

列表界面效果如下所示。

 

2、基于中间表的查询处理

前面的查询处理,主要就是针对没有任何关系的表实体对象的返回处理,但往往我们开发的时候,会涉及到很多相关的表,单独的表相对来说还是比较少,因此对表的关系遍历处理和中间表的关系转换,就需要在数据操作的时候考虑的了。

例如对于字典大类和字典项目的关系,如下所示。

以及在权限管理系统模块中,用户、角色、机构、权限等存在着很多中间表的关系,如下所示。

如对于字典表关系处理,我们采用Queryable<DictDataInfo, DictTypeInfo>的查询处理方式,可以联合两个表对象实体进行联合查询,如下代码所示。

复制代码
        /// <summary>
        /// 根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)
        /// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        public async Task<Dictionary<string, string>> GetDictByDictType(string dictTypeName)
        {
            var query = this.Client.Queryable<DictDataInfo, DictTypeInfo>(
                (d, t) => d.DictType_ID == t.Id && t.Name == dictTypeName)
                .Select(d => d); //联合条件获取对象

            query = query.OrderBy(d => d.DictType_ID).OrderBy(d => d.Seq);//排序
            var list = await query.ToListAsync();//获取列表

            var dict = new Dictionary<string, string>();
            foreach (var info in list)
            {
                if (!dict.ContainsKey(info.Name))
                {
                    dict.Add(info.Name, info.Value);
                }
            }
            return dict;
        }
复制代码

其中的Client对象是DbContext对象实例的Client属性,如下图所示。

这个对象是在DbContext对象中构建的,如下所示。

复制代码
            this.Client = new SqlSugarScope(new ConnectionConfig()
            {
                DbType = this.DbType,
                ConnectionString = this.ConnectionString,
                InitKeyType = InitKeyType.Attribute,
                IsAutoCloseConnection = true, //是否自动关闭连接
                AopEvents = new AopEvents
                {
                    OnLogExecuting = (sql, p) =>
                    {
                        //Log.Information(sql);
                        //Log.Information(string.Join(",", p?.Select(it => it.ParameterName + ":" + it.Value)));
                    }
                }
            });
复制代码

我们查看Queryable,可以看到这个SQLSugar基类函数 Queryable 提供了很多重载函数,也就是它们可以提供更多的表对象进行联合查询的,如下所示。

前面介绍的是外键的一对多的关系查询,通过两个对象之间进行的关系连接,从而实现另一个对象属性的对比查询操作的。

对于中间表的处理,也是类似的情况,我们通过对比中间表的属性,从而实现条件的过滤处理。如下是对于角色中相关关系的中间表查询。

复制代码
        /// <summary>
        /// 根据用户ID获取对应的角色列表
        /// </summary>
        /// <param name="userID">用户ID</param>
        /// <returns></returns>
        private async Task<List<RoleInfo>> GetByUser(int userID)
        {
            var query = this.Client.Queryable<RoleInfo, User_RoleInfo>(
            (t, m) => t.Id == m.Role_ID && m.User_ID == userID)
            .Select(t => t); //联合条件获取对象

            query = query.OrderBy(t => t.CreateTime);//排序
            var list = await query.ToListAsync();//获取列表
            return list;
        }

        /// <summary>
        /// 根据机构获取对应的角色列表(判断机构角色中间表)
        /// </summary>
        /// <param name="ouID">机构的ID</param>
        /// <returns></returns>
        public async Task<List<RoleInfo>> GetRolesByOu(int ouID)
        {
            var query = this.Client.Queryable<RoleInfo, OU_RoleInfo>(
            (t, m) => t.Id == m.Role_ID && m.Ou_ID == ouID)
            .Select(t => t); //联合条件获取对象

            query = query.OrderBy(t => t.CreateTime);//排序
            var list = await query.ToListAsync();//获取列表
            return list;
        }
复制代码

通过联合查询中间表对象信息,可以对它的字段属性进行条件联合,从而获得所需的记录。

这里User_RoleInfo和Ou_RoleInfo表也是根据中间表的属性生成的,不过它们在业务层并没有任何关联操作,也不需要生成对应的Service层,因此只需要生成相关的Model类实体即可。

复制代码
    /// <summary>
    /// 用户角色关联
    /// </summary>
    [SugarTable("T_ACL_User_Role")]
    public class User_RoleInfo
    {         
        /// <summary>
        /// 用户ID
        /// </summary>
        [Required]
        public virtual int User_ID { get; set; }

        /// <summary>
        /// 角色ID
        /// </summary>
        [Required]
        public virtual int Role_ID { get; set; }

    }
复制代码
复制代码
    /// <summary>
    /// 机构角色关联
    /// </summary>
    [SugarTable("T_ACL_OU_Role")]
    public class OU_RoleInfo 
    {         
        /// <summary>
        /// 机构ID
        /// </summary>
        [Required]
        public virtual int Ou_ID { get; set; }

        /// <summary>
        /// 角色ID
        /// </summary>
        [Required]
        public virtual int Role_ID { get; set; }
    }
复制代码

可以看到这两个实体不同于其他实体,它们没有基类继承关系,而一般标准的实体是有的。

复制代码
    /// <summary>
    /// 角色信息
    /// </summary>
    [SugarTable("T_ACL_Role")]
    public class RoleInfo : Entity<int> {  }


    /// <summary>
    /// 功能菜单
    /// </summary>
    [SugarTable("T_ACL_Menu")]
    public class MenuInfo : Entity<string> { }
复制代码

所以我们就不需要构建它们的Service层来处理数据,它的存在合理性只是在于能够和其他实体对象进行表的联合查询处理而且。

最后贴上一个整合SqlSugar处理而完成的系统基础框架的Winform端界面,其中包括用户、组织机构、角色管理、权限管理、菜单管理、日志、字典、客户信息等业务表的处理。

以证所言非虚。

 

系列文章:

基于SqlSugar的开发框架的循序渐进介绍(1)–框架基础类的设计和使用

基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理

基于SqlSugar的开发框架循序渐进介绍(3)– 实现代码生成工具Database2Sharp的整合开发

 

基于SqlSugar的开发框架循序渐进介绍(1)--框架基础类的设计和使用 - 伍华聪 - 博客园

mikel阅读(611)

来源: 基于SqlSugar的开发框架循序渐进介绍(1)–框架基础类的设计和使用 – 伍华聪 – 博客园

在实际项目开发中,我们可能会碰到各种各样的项目环境,有些项目需要一个大而全的整体框架来支撑开发,有些中小项目这需要一些简单便捷的系统框架灵活开发。目前大型一点的框架,可以采用ABP或者ABP VNext的框架,两者整体思路和基础设计类似,不过ABP侧重于一个独立完整的项目框架,开发的时候统一整合处理;而ABP VNext则是以微服务架构为基础,各个模块独立开发,既可以整合在一个项目中,也可以以微服务进行单独发布,并统一通过网关处理进行交流。不管ABP或者ABP VNext框架,都集合了.NET CORE领域众多技术为一体,并且基础类设计上,错综复杂,关系较多,因此开发学习有一定的门槛,中小型项目应用起来有一定的费劲之处。本系列随笔介绍底层利用SQLSugar来做ORM数据访问模块,设计一个简单便捷一点的框架,本篇从基础开始介绍一些框架内容,参照一些ABP/ABP VNext中的一些类库处理,来承载类似条件分页信息,查询条件处理等处理细节。

1、基于SQLSugar开发框架的架构设计

主要的设计模块场景如下所示。

 

为了避免像ABP VNext框架那样分散几十个项目,我们尽可能聚合内容放在一个项目里面。

1)其中一些常用的类库,以及SqlSugar框架的基类放在框架公用模块里面。

2)Winform开发相关的基础界面以及通用组件内容,放在基础Winform界面库BaseUIDx项目中。

3)基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。

4)Winform应用模块,主要就是针对业务开发的WInform界面应用,而WInform开发为了方便,也会将一些基础组件和基类放在了BaseUIDx的Winform专用的界面库里面。

5)WebAPI项目采用基于.net Core6的项目开发,通过调用SugarProjectCore实现相关控制器API的发布,并整合Swagger发布接口,供其他前端界面应用进行调用。

6)纯前端通过API进行调用Web API的接口,纯前端模块可以包含Vue3&Element项目,以及基于EelectronJS应用,发布跨平台的基于浏览器的应用界面,以及其他App或者小程序整合Web API进行业务数据的处理或者展示需要。

如后端开发,我们可以在VS2022中进行管理,管理开发Winform项目、Web API项目等。

Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。这里以权限模块来进行演示整合使用。

 

而纯前端的项目,我们可以基于VSCode或者 HBuilderX等工具进行项目的管理开发工作。

 

2、框架基础类的定义和处理

在开发一个易于使用的框架的时候,主要目的就是减少代码开发,并尽可能通过基类和泛型约束的方式,提高接口的通用性,并通过结合代码生成工具的方式,来提高标准项目的开发效率。

那么我们这里基于SqlSugar的ORM处理,来实现常规数据的增删改查等常规操作的时候,我们是如何进行这些接口的封装处理的呢。

例如,我们对于一个简单的客户信息表,如下所示。

那么它生成的SqlSugar实体类如下所示。

复制代码
    /// <summary>
    /// 客户信息
    /// 继承自Entity,拥有Id主键属性
    /// </summary>
    [SugarTable("T_Customer")]
    public class CustomerInfo : Entity<string>
    {
        /// <summary>
        /// 默认构造函数(需要初始化属性的在此处理)
        /// </summary>
        public CustomerInfo()
        {
            this.CreateTime = System.DateTime.Now;
        }

        #region Property Members

        /// <summary>
        /// 姓名
        /// </summary>
        public virtual string Name { get; set; }

        /// <summary>
        /// 年龄
        /// </summary>
        public virtual int Age { get; set; }

        /// <summary>
        /// 创建人
        /// </summary>
        public virtual string Creator { get; set; }

        /// <summary>
        /// 创建时间
        /// </summary>
        public virtual DateTime CreateTime { get; set; }

        #endregion
    }
复制代码

其中 Entity<string> 是我们根据需要定义一个基类实体对象,主要就是定义一个Id的属性来处理,毕竟对于一般表对象的处理,SqlSugar需要Id的主键定义(非中间表处理)。

复制代码
    [Serializable]
    public abstract class Entity<TPrimaryKey> : IEntity<TPrimaryKey>
    {
        /// <summary>
        /// 实体类唯一主键
        /// </summary>
        [SqlSugar.SugarColumn(IsPrimaryKey = true, ColumnDescription = "主键")]
        public virtual TPrimaryKey Id { get; set; }
    }
复制代码

而IEntity<T>定义了一个接口

复制代码
    public interface IEntity<TPrimaryKey>
    {
        /// <summary>
        /// 实体类唯一主键
        /// </summary>
        TPrimaryKey Id { get; set; }
    }
复制代码

以上就是实体类的处理,我们一般为了查询信息,往往通过一些条件传入进行处理,那么我们就需要定义一个通用的分页查询对象,供我们精准进行条件的处理。

生成一个以***PageDto的对象类,如下所示。

复制代码
    /// <summary>
    /// 用于根据条件分页查询,DTO对象
    /// </summary>
    public class CustomerPagedDto : PagedAndSortedInputDto, IPagedAndSortedResultRequest
    {
        /// <summary>
        /// 默认构造函数
        /// </summary>
        public CustomerPagedDto() : base() { }

        /// <summary>
        /// 参数化构造函数
        /// </summary>
        /// <param name="skipCount">跳过的数量</param>
        /// <param name="resultCount">最大结果集数量</param>
        public CustomerPagedDto(int skipCount, int resultCount) : base(skipCount, resultCount)
        {
        }

        /// <summary>
        /// 使用分页信息进行初始化SkipCount 和 MaxResultCount
        /// </summary>
        /// <param name="pagerInfo">分页信息</param>
        public CustomerPagedDto(PagerInfo pagerInfo) : base(pagerInfo)
        {
        }

        #region Property Members

        /// <summary>
        /// 不包含的对象的ID,用于在查询的时候排除对应记录
        /// </summary>
        public virtual string ExcludeId { get; set; }

        /// <summary>
        /// 姓名
        /// </summary>
        public virtual string Name { get; set; }

        /// <summary>
        /// 年龄-开始
        /// </summary>
        public virtual int? AgeStart { get; set; }
        /// <summary>
        /// 年龄-结束
        /// </summary>
        public virtual int? AgeEnd { get; set; }

        /// <summary>
        /// 创建时间-开始
        /// </summary>
        public DateTime? CreateTimeStart { get; set; }
        /// <summary>
        /// 创建时间-结束
        /// </summary>
        public DateTime? CreateTimeEnd { get; set; }

        #endregion
    }
复制代码

其中PagedAndSortedInputDto, IPagedAndSortedResultRequest都是参考来自于ABP/ABP VNext的处理方式,这样我们可以便于数据访问基类的查询处理操作。

接着我们定义一个基类MyCrudService,并传递如相关的泛型约束,如下所示

复制代码
    /// <summary>
    /// 基于SqlSugar的数据库访问操作的基类对象
    /// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput> : 
        IMyCrudService<TEntity, TKey, TGetListInput>
        where TEntity : class, IEntity<TKey>, new()
        where TGetListInput : IPagedAndSortedResultRequest
复制代码

我们先忽略基类接口的相关实现细节,我们看看对于这个MyCrudService和 IMyCrudService 我们应该如何使用的。

首先我们定义一个应用层的接口ICustomerService如下所示。

复制代码
    /// <summary>
    /// 客户信息服务接口
    /// </summary>
    public interface ICustomerService : IMyCrudService<CustomerInfo, string, CustomerPagedDto>, ITransientDependency
    {

    }
复制代码

然后实现在CustomerService中实现它的接口。

    /// <summary>
    /// 应用层服务接口实现
    /// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService

这样我们对于特定Customer的接口在ICustomer中定义,标准接口直接调用基类即可。

基类MyCrudService提供重要的两个接口,让子类进行重写,以便于进行准确的条件处理和排序处理,如下代码所示。

复制代码
    /// <summary>
    /// 基于SqlSugar的数据库访问操作的基类对象
    /// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    public abstract class MyCrudService<TEntity, TKey, TGetListInput> : 
        IMyCrudService<TEntity, TKey, TGetListInput>
        where TEntity : class, IEntity<TKey>, new()
        where TGetListInput : IPagedAndSortedResultRequest
    {
        /// <summary>
        /// 留给子类实现过滤条件的处理
        /// </summary>
        /// <returns></returns>
        protected virtual ISugarQueryable<TEntity> CreateFilteredQueryAsync(TGetListInput input)
        {
            return EntityDb.AsQueryable();
        }
        /// <summary>
        /// 默认排序,通过ID进行排序
        /// </summary>
        /// <param name="query"></param>
        /// <returns></returns>
        protected virtual ISugarQueryable<TEntity> ApplyDefaultSorting(ISugarQueryable<TEntity> query)
        {
            if (typeof(TEntity).IsAssignableTo<IEntity<TKey>>())
            {
                return query.OrderBy(e => e.Id);
            }
            else
            {
                return query.OrderBy("Id");
            }
        }        
    }
复制代码

对于Customer特定的业务对象来说,我们需要实现具体的条件查询细节和排序条件,毕竟我们父类没有约束确定实体类有哪些属性的情况下,这些就交给子类做最合适了。

复制代码
    /// <summary>
    /// 应用层服务接口实现
    /// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
    {
        /// <summary>
        /// 自定义条件处理
        /// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<CustomerInfo> CreateFilteredQueryAsync(CustomerPagedDto input)
        {
            var query = base.CreateFilteredQueryAsync(input);

            query = query
                .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID
                .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals
                                                                                             //年龄区间查询
                .WhereIF(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value)
                .WhereIF(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value)

                //创建日期区间查询
                .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >= input.CreateTimeStart.Value)
                .WhereIF(input.CreateTimeEnd.HasValue, s => s.CreateTime <= input.CreateTimeEnd.Value)
                ;

            return query;
        }

        /// <summary>
        /// 自定义排序处理
        /// </summary>
        /// <param name="query">可查询LINQ</param>
        /// <returns></returns>
        protected override ISugarQueryable<CustomerInfo> ApplyDefaultSorting(ISugarQueryable<CustomerInfo> query)
        {
            return query.OrderBy(t => t.CreateTime, OrderByType.Desc);

            //先按第一个字段排序,然后再按第二字段排序
            //return base.ApplySorting(query, input).OrderBy(s=>s.Customer_ID).OrderBy(s => s.Seq);
        }
    }
复制代码

通过 CreateFilteredQueryAsync 的精确条件处理,我们就可以明确实体类的查询条件处理,因此对于CustomerPagedDto来说,就是可以有客户端传入,服务后端的基类进行处理了。

如基类的分页条件查询函数GetListAsync就是根据这个来处理的,它的实现代码如下所示。

复制代码
        /// <summary>
        /// 根据条件获取列表
        /// </summary>
        /// <param name="input">分页查询条件</param>
        /// <returns></returns>
        public virtual async Task<PagedResultDto<TEntity>> GetListAsync(TGetListInput input)
        {
            var query = CreateFilteredQueryAsync(input);
            var totalCount = await query.CountAsync();

            query = ApplySorting(query, input);
            query = ApplyPaging(query, input);

            var list = await query.ToListAsync();

            return new PagedResultDto<TEntity>(
               totalCount,
               list
           );
        }
复制代码

而其中 ApplySorting 就是根据条件决定是否选择子类实现的默认排序进行处理的。

复制代码
        /// <summary>
        /// 记录排序处理
        /// </summary>
        /// <returns></returns>
        protected virtual ISugarQueryable<TEntity> ApplySorting(ISugarQueryable<TEntity> query, TGetListInput input)
        {
            //Try to sort query if available
            if (input is ISortedResultRequest sortInput)
            {
                if (!sortInput.Sorting.IsNullOrWhiteSpace())
                {
                    return query.OrderBy(sortInput.Sorting);
                }
            }

            //IQueryable.Task requires sorting, so we should sort if Take will be used.
            if (input is ILimitedResultRequest)
            {
                return ApplyDefaultSorting(query);
            }

            //No sorting
            return query;
        }
复制代码

对于获取单一对象,我们一般提供一个ID主键获取即可。

复制代码
        /// <summary>
        /// 根据ID获取单一对象
        /// </summary>
        /// <param name="id">主键ID</param>
        /// <returns></returns>
        public virtual async Task<TEntity> GetAsync(TKey id)
        {
            return await EntityDb.GetByIdAsync(id);
        }
复制代码

也可以根据用户的Express条件进行处理,在基类我们定义很多这样的Express条件处理,便于子类进行条件处理的调用。如对于删除,可以指定ID,也可以指定条件删除。

复制代码
        /// <summary>
        /// 删除指定ID的对象
        /// </summary>
        /// <param name="id">记录ID</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(TKey id)
        {
            return await EntityDb.DeleteByIdAsync(id);
        }
复制代码
复制代码
/// <summary>
        /// 根据指定条件,删除集合
        /// </summary>
        /// <param name="input">表达式条件</param>
        /// <returns></returns>
        public virtual async Task<bool> DeleteAsync(Expression<Func<TEntity, bool>> input)
        {
            var result = await EntityDb.DeleteAsync(input);
            return result;
        }
复制代码

如判断是否存在也是一样处理

复制代码
        /// <summary>
        /// 判断是否存在指定条件的记录
        /// </summary>
        /// <param name="id">ID 主键</param>
        /// <returns></returns>
        public virtual async Task<bool> IsExistAsync(TKey id)
        {
            var info = await EntityDb.GetByIdAsync(id);
            var result = (info != null);
            return result;
        }

        /// <summary>
        /// 判断是否存在指定条件的记录
        /// </summary>
        /// <param name="input">表达式条件</param>
        /// <returns></returns>
        public virtual async Task<bool> IsExistAsync(Expression<Func<TEntity, bool>> input)
        {
            var result = await EntityDb.IsAnyAsync(input);
            return result;
        }
复制代码

关于Web API的处理,我在随笔《基于SqlSugar的数据库访问处理的封装,在.net6框架的Web API上开发应用》中也有介绍,主要就是先弄好.net6的开发环境,然后在进行相关的项目开发即可。

根据项目的需要,我们定义了一些控制器的基类,用于实现不同的功能。

 

其中ControllerBase是.net core Web API中的标准控制器基类,我们由此派生一个LoginController用于登录授权,而BaseApiController则处理常规接口用户身份信息,而BusinessController则是对标准的增删改查等基础接口进行的封装,我们实际开发的时候,只需要开发编写类似CustomerController基类即可。

BaseApiController没有什么好介绍的,就是封装一下获取用户的身份信息。

可以通过下面代码获取接口用户的Id

        /// <summary>
        /// 当前用户身份ID
        /// </summary>
        protected virtual string? CurrentUserId => HttpContext.User.FindFirst(JwtClaimTypes.Id)?.Value;

而BusinessController控制器则是继承这个BaseApiController即可。通过泛型约束传入相关的对象信息。

复制代码
    /// <summary>
    /// 本控制器基类专门为访问数据业务对象而设的基类
    /// </summary>
    /// <typeparam name="TEntity">定义映射的实体类</typeparam>
    /// <typeparam name="TKey">主键的类型,如int,string等</typeparam>
    /// <typeparam name="TGetListInput">或者分页信息的条件对象</typeparam>
    [Route("[controller]")]
    [Authorize] //需要授权登录访问
    public class BusinessController<TEntity, TKey, TGetListInput> : BaseApiController
        where TEntity : class, IEntity<TKey>, new()
        where TGetListInput : IPagedAndSortedResultRequest
    {
        /// <summary>
        /// 通用基础操作接口
        /// </summary>
        protected IMyCrudService<TEntity, TKey, TGetListInput> _service { get; set; }

        /// <summary>
        /// 构造函数,初始化基础接口
        /// </summary>
        /// <param name="service">通用基础操作接口</param>
        public BusinessController(IMyCrudService<TEntity, TKey, TGetListInput> service)
        {
            this._service = service;
        }

....
复制代码

这个基类接收一个符合基类接口定义的对象作为基类增删删改查等处理方法的接口对象。在具体的CustomerController中的定义处理如下所示。

复制代码
    /// <summary>
    /// 客户信息的控制器对象
    /// </summary>
    public class CustomerController : BusinessController<CustomerInfo, string, CustomerPagedDto>
    {
        private ICustomerService _customerService;

        /// <summary>
        /// 构造函数,并注入基础接口对象
        /// </summary>
        /// <param name="customerService"></param>
        public CustomerController(ICustomerService customerService) :base(customerService)
        {
            this._customerService = customerService;
        }
    }
复制代码

这样就可以实现基础的相关操作了。如果需要特殊的接口实现,那么定义方法实现即可。

类似字典项目中的控制器处理代码如下所示。定义好HTTP方法,路由信息等即可。

复制代码
        /// <summary>
        /// 根据字典类型ID获取所有该类型的字典列表集合(Key为名称,Value为值)
        /// </summary>
        /// <param name="dictTypeId">字典类型ID</param>
        /// <returns></returns>
        [HttpGet]
        [Route("by-typeid/{dictTypeId}")]
        public async Task<Dictionary<string, string>> GetDictByTypeID(string dictTypeId)
        {
            return await _dictDataService.GetDictByTypeID(dictTypeId);
        }

        /// <summary>
        /// 根据字典类型名称获取所有该类型的字典列表集合(Key为名称,Value为值)
        /// </summary>
        /// <param name="dictTypeName">字典类型名称</param>
        /// <returns></returns>
        [HttpGet]
        [Route("by-typename/{dictTypeName}")]
        public async Task<Dictionary<string, string>> GetDictByDictType(string dictTypeName)
        {
            return await _dictDataService.GetDictByDictType(dictTypeName);
        }
复制代码

 

系列文章:

基于SqlSugar的开发框架的循序渐进介绍(1)–框架基础类的设计和使用

基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理

基于SqlSugar的开发框架循序渐进介绍(3)– 实现代码生成工具Database2Sharp的整合开发

 

基于SqlSugar的开发框架循序渐进介绍(3)-- 实现代码生成工具Database2Sharp的整合开发 - 伍华聪 - 博客园

mikel阅读(503)

来源: 基于SqlSugar的开发框架循序渐进介绍(3)– 实现代码生成工具Database2Sharp的整合开发 – 伍华聪 – 博客园

我喜欢在一个项目开发模式成熟的时候,使用代码生成工具Database2Sharp来配套相关的代码生成,对于我介绍的基于SQLSugar的开发框架,从整体架构确定下来后,我就着手为它们量身定做相关的代码开发,这样可以在后续整合项目功能的时候,利用代码生成工具快速的生成所需要模块的骨架代码,然后在这个基础上逐渐增加自定义的内容即可,方便快捷。本篇随笔介绍基于SQLSugar的开发框架,对框架中涉及到的各个分层或者模块代码进行生成的处理。

1、回顾项目的架构和模块内容

在前面几篇随笔中,大概介绍过了基于SQLSugar的开发框架主要的设计模块,场景如下所示。

基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。其中Service里面一些框架基类和接口定义,统一也放在公用类库里面。

Winform界面,我们可以采用基于.net Framework开发或者.net core6进行开发均可,因为我们的SugarProjectCore项目是采用.net Standard模式开发,兼容两者。

这里以权限模块来进行演示整合使用,我在构建代码生成工具代码模板的时候,反复利用项目中测试没问题的项目代码指导具体的模板编写,这样编写出来的模板就会完美符合实际的项目需要了。

在项目代码及模板完成后,利用代码生成工具快速生成代码,相互促进情况下,也完成了Winform项目的界面代码生成,生成包括普通的列表界面,以及主从表Winform界面代码生成。

最后权限系统的Winform项目如下所示。

在前面随笔《基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理》中介绍了基础功能的一些处理,其中也介绍到了Winform界面端的界面效果,这个以SqlSugar底层处理,最终把权限、字典等模块整合到一起,完成一个项目开发所需要的框架结构内容。整个系统包括用户管理、组织机构管理、角色管理、功能权限管理、菜单管理、字段权限管理、黑白名单、操作日志、字典管理、客户信息等模块内容。

在代码生成工具中,我们整合了基于SqlSugar的开发框架的项目代码生成,包括框架基础的代码生成,以及Winform界面代码生成两个部分,框架项目及Winform界面效果如上图所示。

 

2、整合代码生成工具Database2Sharp进行SqlSugar框架代码生成

前面随笔介绍过基于SqlSugar核心Core项目的组成。

基础核心数据模块SugarProjectCore,主要就是开发业务所需的数据处理和业务逻辑的项目,为了方便,我们区分Interface、Modal、Service三个目录来放置不同的内容,其中Modal是SqlSugar的映射实体,Interface是定义访问接口,Service是提供具体的数据操作实现。

对于Modal层的类代码生成,常规的普通表(非中间表),我们根据项目所需要,生成如下代码。目的是利用它定义好对应的主键Id,并通过接口约束实体类。

复制代码
    /// <summary>
    /// 客户信息
    /// 继承自Entity,拥有Id主键属性
    /// </summary>
    [SugarTable("T_Customer")]
    public class CustomerInfo : Entity<string>
复制代码

而对于中间表,我们不要它的继承继承关系。

复制代码
    /// <summary>
    /// 用户角色关联
    /// </summary>
    [SugarTable("T_ACL_User_Role")]
    public class User_RoleInfo
    {
    }
复制代码

只需要简单的标注好SugarTable属性,让他可以和其他业务表进行关联查询即可。

复制代码
        /// <summary>
        /// 根据用户ID获取对应的角色列表
        /// </summary>
        /// <param name="userID">用户ID</param>
        /// <returns></returns>
        private async Task<List<RoleInfo>> GetByUser(int userID)
        {
            var query = this.Client.Queryable<RoleInfo, User_RoleInfo>(
            (t, m) => t.Id == m.Role_ID && m.User_ID == userID)
            .Select(t => t); //联合条件获取对象

            query = query.OrderBy(t => t.CreateTime);//排序
            var list = await query.ToListAsync();//获取列表
            return list;
        }
复制代码

对于接口层的类,我们只需要按固定的继承关系处理好,以及类的名称变化即可。

复制代码
    /// <summary>
    /// 系统用户信息,应用层服务接口定义
    /// </summary>
    public interface IUserService : IMyCrudService<UserInfo, int, UserPagedDto>, ITransientDependency
    {
    }
复制代码

其中 IMyCrudService 是我们定义的基类接口,保存常规的增删改查等的处理基类,通过传入泛型进行约束接口参数类型和返回值。

基类接口尽可能满足实际项目接口所需,这样可以减少子类的代码编写,以及获得统一调用基类函数的便利。

 

对于中间表,我们除了生成实体类外,不需要生成其他接口和接口实现层,因为我们不单独调用它们。

对于具体业务对象对应的接口实现,我们除了确定它的继承关系外,我们还会重写它们的一些基类函数,从而实现更加精准的处理。

接口实现类的定义如下所示。

复制代码
    /// <summary>
    /// 应用层服务接口实现
    /// </summary>
    public class CustomerService : MyCrudService<CustomerInfo, string, CustomerPagedDto>, ICustomerService
    {

    }
复制代码

一般情况下,我们至少需要在子类重写 CreateFilteredQueryAsync 和 ApplyDefaultSorting 两个函数,前者是条件的查询处理,后者是默认的排序处理操作。

复制代码
        /// <summary>
        /// 自定义条件处理
        /// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<CustomerInfo> CreateFilteredQueryAsync(CustomerPagedDto input)
        {
            var query = base.CreateFilteredQueryAsync(input);

            query = query
                .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t => t.Id != input.ExcludeId) //不包含排除ID
                .WhereIF(!input.Name.IsNullOrWhiteSpace(), t => t.Name.Contains(input.Name)) //如需要精确匹配则用Equals
                                                                                             //年龄区间查询
                .WhereIF(input.AgeStart.HasValue, s => s.Age >= input.AgeStart.Value)
                .WhereIF(input.AgeEnd.HasValue, s => s.Age <= input.AgeEnd.Value)

                //创建日期区间查询
                .WhereIF(input.CreateTimeStart.HasValue, s => s.CreateTime >= input.CreateTimeStart.Value)
                .WhereIF(input.CreateTimeEnd.HasValue, s => s.CreateTime <= input.CreateTimeEnd.Value)
                ;

            return query;
        }

        /// <summary>
        /// 自定义排序处理
        /// </summary>
        /// <param name="query">可查询LINQ</param>
        /// <returns></returns>
        protected override ISugarQueryable<CustomerInfo> ApplyDefaultSorting(ISugarQueryable<CustomerInfo> query)
        {
            return query.OrderBy(t => t.CreateTime, OrderByType.Desc);

            //如果先按第一个字段排序,然后再按第二字段排序,示例代码
            //return base.ApplySorting(query, input).OrderBy(s=>s.Customer_ID).OrderBy(s => s.Seq);
        }
复制代码

根据这些规则,编写我们所需的模板代码,让我们选择的数据库表名称、注释,以及表字段的名称、类型、注释,外键主键关系等信息为我们模板所用。

如下所示代码是NVelocity模板代码,用于生成上面的条件查询处理的,可以稍作了解。

复制代码
        /// <summary>
        /// 自定义条件处理
        /// </summary>
        /// <param name="input">查询条件Dto</param>
        /// <returns></returns>
        protected override ISugarQueryable<${ClassName}Info> CreateFilteredQueryAsync(${ClassName}PagedDto input)
        {
            var query = base.CreateFilteredQueryAsync(input);
             query = query
#if(${PrimaryKeyNetType}=="string")
                .WhereIF(!input.ExcludeId.IsNullOrWhiteSpace(), t=>t.Id != input.ExcludeId) //不包含排除ID
#else
                .WhereIF(input.ExcludeId.HasValue, t=>t.Id != input.ExcludeId) //不包含排除ID
#end
#foreach($EntityProperty in $EntityPropertyList) 
#if(${EntityProperty.ColumnInfo.IsForeignKey} || ${EntityProperty.PropertyName}  == "Status" || ${EntityProperty.PropertyName}  == "State" || ${EntityProperty.PropertyName}  == "PID" || ${EntityProperty.PropertyName}  == "Deleted")
                .WhereIF(#if(${EntityProperty.ColumnInfo.IsNumeric})input.${EntityProperty.PropertyName}.HasValue#else!input.${EntityProperty.PropertyName}.IsNullOrWhiteSpace()#end, s => s.${EntityProperty.PropertyName} == input.${EntityProperty.PropertyName})
#elseif(${EntityProperty.ColumnInfo.IsDateTime} || ${EntityProperty.ColumnInfo.IsNumeric})
                //${EntityProperty.Description}区间查询
                .WhereIF(input.${EntityProperty.PropertyName}Start.HasValue, s => s.${EntityProperty.PropertyName} >= input.${EntityProperty.PropertyName}Start.Value)
                .WhereIF(input.${EntityProperty.PropertyName}End.HasValue, s => s.${EntityProperty.PropertyName} <= input.${EntityProperty.PropertyName}End.Value)
#elseif(${EntityProperty.ColumnInfo.NetType.Alias.ToLower()} != "byte[]" && ${EntityProperty.ColumnInfo.Name.Name.ToString()} != "AttachGUID")
                .WhereIF(#if(${EntityProperty.NetType.EndsWith("?")})input.${EntityProperty.PropertyName}.HasValue, t => t.${EntityProperty.PropertyName} == input.${EntityProperty.PropertyName}#else!input.${EntityProperty.PropertyName}.IsNullOrWhiteSpace(), t => t.${EntityProperty.PropertyName}.Contains(input.${EntityProperty.PropertyName})#end) //如需要精确匹配则用Equals
#end ##endif
#end

#if(${HasCreationTime}) 
                //创建日期区间查询
                .WhereIF(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value)
                .WhereIF(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value)
#else
                //创建日期区间查询(参考)
                //.WhereIF(input.CreationTimeStart.HasValue, s => s.CreationTime >= input.CreationTimeStart.Value)
                //.WhereIF(input.CreationTimeEnd.HasValue, s => s.CreationTime <= input.CreationTimeEnd.Value)
#end;
            return query;
        }
复制代码

 

当我们完成所需的模板代码开发后,就在代码生成工具主体界面中整合相关的生成功能菜单,界面效果如下所示。

通过菜单选择【SqlSugar框架代码生成】,进一步选择数据库中的表进行生成,一步步处理即可,最后列出所选数据库表,并确认生成操作,即可生成SqlSugar框架核心项目的代码,如下图所示。

选择表进行生成后,生成的实体模型类如下所示,包括生成了中间表的实体类。

 

而接口实现则是根据具体的业务对象规则进行生成。

 

 

3、SqlSugar项目中Winform界面的生成

Winform界面包括普通列表/编辑界面处理,以及主从表界面处理两个部分,如下图所示。

生成的简单业务表界面,包括分页列表展示界面,在列表界面中整合查看、编辑、新增、删除、导入、导出、查询/高级查询等功能,整合的编辑界面也是依据数据库表的信息进行生成的。

列表界面和编辑界面效果如下所示。

而主从表界面生成的效果如下所示。

我们看看生成的Winform列表界面代码,如下所示。

另外我们把一些常用的处理逻辑放在函数中统一处理,如AddData、EditData、DeleteData、BindData、GetData、ImportData、ExportData等等,如下所示。

 

在获取数据的时候,我们根据用户的条件,构建一个分页查询对象传递,调用接口获得数据后,进行分页控件的绑定处理即可。

复制代码
        /// <summary>
        /// 获取数据
        /// </summary>
        /// <returns></returns>
        private async Task<IPagedResult<CustomerInfo>> GetData()
        {
            CustomerPagedDto pagerDto = null;
            if (advanceCondition != null)
            {
                //如果有高级查询,那么根据输入信息构建查询条件
                pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo);
                pagerDto = dlg.GetPagedResult(pagerDto);
            }
            else
            {
                //构建分页的条件和查询条件
                pagerDto = new CustomerPagedDto(this.winGridViewPager1.PagerInfo)
                {
                    //添加所需条件
                    Name = this.txtName.Text.Trim(),
                };

                //日期和数值范围定义
                //年龄,需在CustomerPagedDto中添加 int? 类型字段AgeStart和AgeEnd
                var Age = new ValueRange<int?>(this.txtAge1.Text, this.txtAge2.Text); //数值类型
                pagerDto.AgeStart = Age.Start;
                pagerDto.AgeEnd = Age.End;

                //创建时间,需在CustomerPagedDto中添加 DateTime? 类型字段CreationTimeStart和CreationTimeEnd
                var CreationTime = new TimeRange(this.txtCreationTime1.Text, this.txtCreationTime2.Text); //日期类型
                pagerDto.CreateTimeStart = CreationTime.Start;
                pagerDto.CreateTimeEnd = CreationTime.End;
            }

            var result = await BLLFactory<CustomerService>.Instance.GetListAsync(pagerDto);
            return result;
        }
复制代码

如果是高级查询,我们则是根据传入分页查询对象的属性在高级查询对话框中进行赋值,然后获得对象后进行查询获得记录的。

 

在代码生成工具中,我们根据实际项目的代码,定义好对应的模板文件,如下所示。

最后在生成代码的时候,整合这些NVelocity的模板文件,根据表对象的信息,生成对应的文件供我们开发使用即可。

复制代码
            #region Model 实体部分

            string entityTemplateFile = ProjectPath + "Templates/Entity.cs.vm";
            var entityAdapter = new SugarEntityAdapter(databaseInfo, selectedTableNames, entityTemplateFile);
            entityAdapter.DirectoryOfOutput = mainSetting.RootNameSpace + "/Core/Modal";
            entityAdapter.Execute();

            #endregion

            #region Interface部分和Application部分

            var appInterface = new SugarServiceInterfaceAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/IService.cs.vm", databaseTypeName);
            appInterface.DirectoryOfOutput = mainSetting.RootNameSpace + "/Core/Interface";
            appInterface.Execute();

            var appService = new SugarServiceAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/Service.cs.vm", databaseTypeName);
            appService.DirectoryOfOutput = mainSetting.RootNameSpace + "/Core/Service";
            appService.Execute();

            #endregion

            #region Web API Controller 部分

            var controller = new SugarControllerAdapter(databaseInfo, selectedTableNames, ProjectPath + "Templates/Controller.cs.vm", databaseTypeName);
            controller.DirectoryOfOutput = mainSetting.RootNameSpace + "/Controller";
            controller.Execute();

            #endregion
复制代码

 

系列文章:

基于SqlSugar的开发框架的循序渐进介绍(1)–框架基础类的设计和使用

基于SqlSugar的开发框架循序渐进介绍(2)– 基于中间表的查询处理

基于SqlSugar的开发框架循序渐进介绍(3)– 实现代码生成工具Database2Sharp的整合开发

sql server 性能调优之 死锁排查 - 花阴偷移 - 博客园

mikel阅读(474)

来源: sql server 性能调优之 死锁排查 – 花阴偷移 – 博客园

一.概述

记得以前客户在使用软件时,有偶发出现死锁问题,因为发生的时间不确定,不好做问题的重现,当时解决问题有点棘手了。现总结下查看死锁的常用二种方式。

1.1 第一种是图形化监听:

SQLServer –>工具–> SQL server profiler   登录后在跟踪属性中选择如下图:

监听到的死锁图形如下图

这里的描述大致是:有二个进程 一个进程ID是96, 另一个ID是348.   系统自动kill 掉了进程ID:96,保留了进程ID:348 的事务Commit。

上面死锁是由于批量更新出现PAG范围锁, 双方进程在同一分区索引资源上。ID96,348都请求想获取更新锁(U),各占排它锁(x)不释放,直到锁超时。

 

1.2 第二种是使用日志跟踪(errorlog)

以全局方式打开指定的跟踪标记

DBCC TRACEON(1222,-1)
DBCC TRACEON(1204,-1)

使用  EXEC master..xp_readerrorlog 查看日志。 由于记录的死锁信息太多,贴出几个重点说下(红色加粗表示)

复制代码
Deadlock encountered .... Printing deadlock information
Wait-for graph
NULL
Node:1 
PAGE: 7:1:6229275 CleanCnt:2 Mode:IX Flags: 0x3
Grant List 3:
Owner:0x00000004E99B7880 Mode: IX Flg:0x40 Ref:1 Life:02000000 SPID:219 ECID:0 XactLockInfo: 0x0000000575C7E970
SPID: 219 ECID: 0 Statement Type: UPDATE Line #: 84
Input Buf: Language Event: exec proc_PUB_StockDataImport
Requested by: 
ResType:LockOwner Stype:'OR'Xdes:0x0000000C7A905D30 Mode: U SPID:64 BatchID:0 ECID:59 TaskProxy:(0x0000000E440AAFE0) Value:0x8d160240 Cost:(0/0)
NULL

Node:2 
PAGE: 7:1:5692366 CleanCnt:2 Mode:U Flags: 0x3
Grant List 3:
Owner:0x0000000D12099B80 Mode: U Flg:0x40 Ref:0 Life:00000001 SPID:64 ECID:0 XactLockInfo: 0x000000136B4758F0
SPID: 64 ECID: 0 Statement Type: UPDATE Line #: 108
Input Buf: RPC Event: Proc [Database Id = 7 Object Id = 907150277]
复制代码

——– node:1 部分显示的几个关键信息:

PAGE 7:1:6229275  (所在数据库ID 7, 1分区, 6229275行数)

Mode: IX  锁的模式  意向排它锁

SPID: 219  进程ID

Event: exec proc_PUB_StockDataImport  执行的存储过程名

——-node:2 部分显示的几个关键信息

PAGE 7:1:5692366  (所在数据库ID 7, 1分区,5692366行数)

Mode:U 锁的模式  更新锁

RPC Event: Proc 远程调用

SPID: 64  进程ID

复制代码
Victim Resource Owner:
ResType:LockOwner Stype:'OR'Xdes:0x0000000C7A905D30 Mode: U SPID:64 BatchID:0 ECID:59 TaskProxy:(0x0000000E440AAFE0) Value:0x8d160240 Cost:(0/0)
deadlock-list
deadlock victim=process956f4c8
process-list
process id=process956f4c8 taskpriority=0 logused=0 waitresource=PAGE: 7:1:6229275 waittime=2034 ownerId=2988267079 transactionname=UPDATE 
lasttranstarted=2018-04-19T13:54:00.360 XDES=0xc7a905d30 lockMode=U schedulerid=24 kpid=1308 status=suspended spid=64 sbid=0 ecid=59 priority=0 trancount=0 
lastbatchstarted=2018-04-19T13:53:58.033 lastbatchcompleted=2018-04-19T13:53:58.033 clientapp=.Net SqlClient Data Provider hostname=VMSERVER76 hostpid=16328 
isolationlevel=read committed (2) xactid=2988267079 currentdb=7 lockTimeout=4294967295 clientoption1=671088672 clientoption2=128056
executionStack
frame procname=Test.dbo.proc_CnofStock line=108 stmtstart=9068 stmtend=9336 sqlhandle=0x03000700c503123601ba25019ca800000100000000000000
update dbo.pub_stock
set UpdateTime=GETDATE()
from pub_stock a
join PUB_PlatfromStocktemp b on a.GUID=b.StockGuid
复制代码

从上面的信息能看到kill 掉的是进程id是process956f4c8,

    进程spid=64

lockMode=U 获取更新锁

isolationlevel=read committed

executionStack 执行的堆信息:

                  存储名  procname=Test.dbo.proc_CnofStock

                  语句    update dbo.pub_stock set UpdateTime=GETDATE()   ..

clientapp   发起事件的来源

 

1.3 最后总结   避免死锁的解决方法

按同一顺序访问对象。

优化索引,避免全表扫描,减少锁的申请数目.

避免事务中的用户交互。

使用基于行版本控制的隔离级别。

将事务默认隔离级别的已提交读改成快照

SET TRANSACTION ISOLATION LEVEL SNAPSHOT

使用nolock去掉共享锁,但死锁发生在u锁或x锁上,则nolock不起作用

升级锁颗粒度(页锁,表锁), 以阻塞还代替死锁

sql server 性能调优之 资源等待 LCk - 花阴偷移 - 博客园

mikel阅读(488)

来源: sql server 性能调优之 资源等待 LCk – 花阴偷移 – 博客园

 一.  概述

这次介绍实例级别资源等待LCK类型锁的等待时间,关于LCK锁的介绍可参考 “sql server 锁与事务拨云见日”。下面还是使用sys.dm_os_wait_stats 来查看,并找出耗时最高的LOK锁。

复制代码
select wait_type,
waiting_tasks_count,
wait_time_ms ,
max_wait_time_ms,
signal_wait_time_ms
from sys.dm_os_wait_stats
where wait_type like 'LCK%' 
order by  wait_time_ms desc
复制代码

查出如下图所示:

  1.  分析介绍

重点介绍几个耗时最高的锁含义:

LCK_M_IX: 正在等待获取意向排它锁。在增删改查中都会有涉及到意向排它锁。
LCK_M_U: 正在等待获取更新锁。 在修改删除都会有涉及到更新锁。
LCK_M_S:正在等待获取共享锁。 主要是查询,修改删除也都会有涉及到共享锁。
LCK_M_X:正在等待获取排它锁。在增删改中都会有涉及到排它锁。
LCK_M_SCH_S:正在等待获取架构共享锁。防止其它用户修改如表结构。
LCK_M_SCH_M:正在等待获取架构修改锁 如添加列或删除列 这个时候使用的架构修改锁。

下面表格是统计分析

锁类型 锁等待次数 锁等待总时间(秒) 平均每次等待时间(毫秒) 最大等待时间
LCK_M_IX 26456 5846.871 221 47623
LCK_M_U 34725 425.081 12 6311
LCK_M_S 613 239.899 391 4938
LCK_M_X 4832 77.878 16 4684
LCK_M_SCH_S 397 77.832 196 6074
LCK_M_SCH_M 113 35.783 316 2268

  注意: wait_time_ms 时间里,该时间表包括了signal_wait_time_ms信号等待时间,也就是说wait_time_ms不仅包括了申请锁需要的等待时间,还包括了线程Runnable 的信号等待。通过这个结论也能得出max_wait_time_ms 最大等待时间不仅仅只是锁申请需要的等待时间。

 

2. 重现锁等待时间

--  重置
DBCC SQLPERF ('sys.dm_os_wait_stats', CLEAR);

--  会话1 更新SID=92525000, 未提交
begin tran 
update [dbo].[PUB_StockTestbak] set model='mmtest' where sid=92525000
-- 会话2 查询该ID, 由于会话1更新未提交 占用x锁,这里查询将阻塞
select * from [PUB_StockTestbak] where sid=92525000

手动取消会话2的查询,占用时间是61秒,如下图:

再来统计资源等待LCK,如下图 :

总结:可以看出资源等待LCK的统计信息还是非常正确的。所以找出性能消耗最高的锁类型,去优化是很有必要。比较有针对性的解决阻塞问题。

3. 造成等待的现象和原因

现象:

(1)  用户并发越问越多,性能越来越差。应用程序运行很慢。

(2)  客户端经常收到错误 error 1222 已超过了锁请求超时时段。

(3)  客户端经常收到错误 error 1205 死锁。

(4)  某些特定的SQL 不能及时返回应用端。

原因:

(1) 用户并发访问越多,阻塞就会越来越多。

(2) 没有合理使用索引,锁申请的数量多。

(3) 共享锁没有使用nolock, 查询带来阻塞。 好处是必免脏读。

(4) 处理的数据过大。比如:一次更新上千条,且并发多。

(5) 没有选择合适的事务隔离级别,复杂的事务处理等。

4.  优化锁的等待时间

在优化锁等待优化方面,有很多切入点 像前几篇中有介绍 CPU和I/O的耗时排查和处理方案。 我们也可以自己写SQL来监听锁等待的sql 语句。能够知道哪个库,哪个表,哪条语句发生了阻塞等待,是谁阻塞了它,阻塞的时间。

从上面的平均每次等待时间(毫秒),最大等待时间 作为参考可以设置一个阀值。 通过sys.sysprocesses 提供的信息来统计, 关于sys.sysprocesses使用可参考 “sql server 性能调优 从用户会话状态分析”。 通过该视图 监听一段时间内的阻塞信息。可以设置每10秒跑一次监听语句,把阻塞与被阻塞存储下来。

思想如下:

复制代码
-- 例如 找出被阻塞会话ID 如时间上是2秒 以及谁阻塞了它的会话ID
SELECT spid,blocked #monitorlock FROM sys.sysprocesses 
where blocked>0 and    waittime>2000 

-- 通过while或游标来一行行获取临时表的 会话ID,阻塞ID,通过exec动态执行来获取sql语句文本 进行存储
exec('DBCC INPUTBUFFER('+@spid+')') 

exec('DBCC INPUTBUFFER('+@blocked+')')
复制代码

 

sql server 锁与事务拨云见日(下) - 花阴偷移 - 博客园

mikel阅读(458)

来源: sql server 锁与事务拨云见日(下) – 花阴偷移 – 博客园

在锁与事务系列里已经写完了上篇中篇,这次写完下篇。这个系列俺自认为是有条不紊的进行,但感觉锁与事务还是有多很细节没有讲到,温故而知新可以为师矣,也算是一次自我提高总结吧,也谢谢大伙的支持。在上一篇的末尾写了事务隔离级别的不同表现,还没写完,只写到了重复读的不同隔离表现,这篇继续写完序列化,快照的不同隔离表现,事务隔离级别的总结。最后讲下事务的死锁,事务的分布式,事务的并发检查。

一. 事务隔离不同表现

设置序列化

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE

设置行版本控制已提交读

ALTER DATABASE  Test  SET  READ_COMMITTED_SNAPSHOT on; 
SET TRANSACTION ISOLATION LEVEL READ COMMITTED

设置快照隔离

ALTER DATABASE Test
SET ALLOW_SNAPSHOT_ISOLATION ON;
SET TRANSACTION ISOLATION LEVEL SNAPSHOT

1.1 已重复读和序列化与其它事务并发,的区别如下表格:

可重复读 序列化 其它事务
SET TRANSACTION ISOLATION

LEVEL REPEATABLE READ

SET TRANSACTION ISOLATION

LEVEL SERIALIZABLE

begin tran

select count(*) from product

where memberID=9708

这里显示500条数据,事务还没有结束

begin tran

select count(*) from product

where memberID=9708

这里显示500条数据,事务还没有结束

begin tran

insert into product

values(‘test2’,9708)

其它事务里,想增加一条数据。

如果并发的事务是可重复读,

这条数据可以插入成功。

如果并发的事务是序列化,

这条数据插入是阻塞的。

select count(*) from product

where memberID=9708

在事务里再次查询时,发现显示501条数据

 select count(*) from productwhere memberID=9708

在事务再次查询时,还是显示500条数据

 commit tran

在一个事务里,对批数据多次读取,符合条件

的行数会不一样。

 commit tran

事务结束

 如果并发是可序列化并且commit,

其它事务新增阻塞消失,插入开始执行。

1.2 已提交读、行版本控制已提交读、快照隔离,与其它事务并发,的区别如下表格:

已提交读 行版本控制已提交读 快照隔离 其它事务
SET TRANSACTION ISOLATION

LEVEL READ COMMITTED

ALTER DATABASE Test SET
READ_COMMITTED_SNAPSHOT
ON;

SET TRANSACTION ISOLATION
LEVEL READ COMMITTED

ALTER DATABASE TEST SET
ALLOW_SNAPSHOT_ISOLATION
ON;

SET TRANSACTION ISOLATION
LEVEL SNAPSHOT

begin tran

select model from product
where sid=9708

得到值为test

begin tran

select model from product
where sid=9708

得到值为test

begin tran

select model from product
where sid=9708

得到值为test

begin tran
update product set
model=’test1′
where sid=1
select model from product
where sid=9708

事务里再次查询 阻塞

select model from product
where sid=9708

事务里再次查询值为test, 读到行版本

select model from product
where sid=9708
事务里再次查询值为test,读到行版本
 阻塞解除,再次查询返回 test1 再次查询 test1
其它事务提交后,这里读到的是新
(修改后的)数据
再次查询 test

其它事务提交后,这里读取还是旧数据
(行版本数据)

 commit tran
 事务里updaate修改 修改成功  事务里updaate修改 修改成功  事务里updaate修改, 修改失败报错  

二. 事务总结

2.1   事务不同隔离级别的优缺点,以及使用场景 如下表格:

隔离级别          优点 缺点 使用场景
未提交读  读数据的时候,不申请共享锁,所以不会被阻塞 读到的数据,可能会脏读,不一致。 如做年度,月度统计报表,数据不一定要非常精确
已提交读   比较折中,而且是推荐的默认设置 有可能会阻塞,在一个事务里,多次读取相同的数据行,得到的结果可能不同。 一般业务都是使用此场景
可重复读 在一个事务里,多次读取相同的数据行,得到的结果可保证一致、 更严重的阻塞,在一个事务里,读取符合某查询的行数,会有变化(这是因为事务里允许新增)  如当我们在事务里需要,多次统计查询范围条件行数, 做精确逻辑运算时,需要考虑逻辑是否会前后不一致.
可序列化 最严重格的数据保护,读取符合某查询的行数,不会有变化(不允许新增)。 其它事务的增,删,改,查 范围内都会阻塞  如当我们在写事务时,不用考虑新增数据带来的逻辑错误。
行版本控制已提交读 阻塞大大减少(读与读不阻塞,读与写不阻塞)

阻塞减少,能读到新数据
大多情况下行版本控制的已提交读比快照隔离更受欢迎:
1、RCSI比SI占用更少的tempdb空间 。
2、RCSI支持分布式事务,而SI不支持 。
3、RCSI不会产生更新冲突 。
4、RCSI无需再应用程序端作任何修改。唯一要更改的只是一个数据库选项。

写与写还是会阻塞,行版本是存放在tempdb里,数据修改的越多,需要

存储的信息越多,维护行版本就

需要越多的的开销

如果默认方式阻塞比较严重,推荐用行版本控制已提交读,改善性能
快照隔离 阻塞大大减少(读与读不阻塞,读与写不阻塞)

阻塞减少,有可能读到旧数据
1、不太可能由于更新冲突而导致事务必须回滚得情况
2、需要基于运行时间长、能保证时间点一致性的多语句来生成报表的情况

维护行版本需要额外开销,且可能读到旧的数据 允许读取稍微比较旧版本信息的情况下

2.2 锁的隔离级别(补充)

了解了事务的隔离级别,锁也是有隔离级别的,只是它针对是单独的SQL查询。下面包括显示如下

     select  COUNT(1) from dbo.product(HOLDLOCK)
HOLDLOCK 在该表上保持共享锁,直到整个事务结束,而不是在语句执行完立即释放所添加的锁。

与SERIALIZABLE一样

NOLOCK 不添加共享锁和排它锁,仅应用于SELECT语句

与READ UNCOMMITTED一样

PAGLOCK 指定添加页锁(否则通常可能添加表锁)。
READPAST 跳过已经加锁的数据行, 仅应用于READ COMMITTED隔离性级别下事务操作中的SELECT语句操作
ROWLOCK 使用行级锁,而不使用粒度更粗的页级锁和表级锁

建议中用在UPDATE和DELETE语句中。

TABLOCKX 表上使用排它锁, 这个锁可以阻止其他事务读或更新这个表的数据
UPDLOCK 指定在读表中数据时设置更新锁(update lock)而不是设置共享锁,作用是允许用户先读取数据(而且不阻塞其他用户读数据),并且保证在后来再更新数据时,这一段时间内这些数据没有被其他用户修改

五.分布式事务

分布式事务是跨越两个或多个称为资源管理器的服务器。 称为事务管理器的服务器组件必须在资源管理器之间协调事务管理。在 .NET Framework 中,分布式事务通过 System.Transactions 命名空间中的 API 进行管理。 如果涉及多个永久资源管理器,System.Transactions API 会将分布式事务处理委托给事务监视器,例如 Microsoft 分布式事务协调程序 (MS DTC),在Windows服务里该服务叫Distributed Transaction Coordinator 默认未启动。

SQL server里 分布式是通过BEGIN DISTRIBUTED TRANSACTION 的T-SQL来实现,是分布式事务处理协调器 (MS DTC) 管理的 Microsoft 分布式事务的起点。执行 BEGIN DISTRIBUTED TRANSACTION 语句的 SQL Server 数据库引擎的实例是事务创建者。并控制事务的完成。 当为会话发出后续 COMMIT TRANSACTION 或 ROLLBACK TRANSACTION 语句时,控制事务实例请求 MS DTC 在所涉及的所有实例间管理分布式事务的完成(事务级别的快照隔离不支持分布式事务)。

在执行T-sql里 查询多个数据库主要是通过引用链接服务器的分布式查询,下面添加了RemoteServer链接服务器

复制代码
USE AdventureWorks2012;  
GO  
BEGIN DISTRIBUTED TRANSACTION;  
-- Delete candidate from local instance.  
DELETE AdventureWorks2012.HumanResources.JobCandidate  
    WHERE JobCandidateID = 13;  
-- Delete candidate from remote instance.  
DELETE RemoteServer.AdventureWorks2012.HumanResources.JobCandidate  
    WHERE JobCandidateID = 13;  
COMMIT TRANSACTION;  
GO
复制代码

六.事务死锁

6.1 在关系型数据库里都有死锁的概念,在并发访问量高时,事务里或者T-sql大批量操作(特别是修改删除结果集),都有可能导致死锁。死锁是由两个互相阻塞的线程组成也称为抱死。sql server死锁监视器进程会定期检查死锁,默认间隔为5秒,会自动判断将回滚开销影响最少的事务作为死锁牺牲者,并收到1025 错误,消息模板来自master.dbo.sysmessages表的where error=1205。当发生死锁时要了解两方进程的sessionid各是多少, 各会话的查询语句,冲突资源是什么。请查看死锁的分析排查

会产生死锁的资源主要是:锁 (就是上篇讲的数据行,页,表等资源),其它的死锁包括如:1. 工作者线程调度程序或CLR同步对象。2.两个线程需要更多内存,但获得授权前一个必须等待另一个。3.同一个查询的并行线程。4.多动态结果集(MARS)资源线程内部冲突。这四种很少出现死锁,重点只要关注锁资源带来的死锁。

6.2 下面事务锁资源产生死锁的原理:

1. 事务T1和事务T2 分别占用共享锁RID第1行和共享锁RID第2行。

2. 事务T1更新RID2试图获取X阻塞,事务T2更新RID2试图获取X阻塞。

3.  事务各自占有共享锁未释放,而要申请对方X锁会排斥一切锁

6.3 死锁与阻塞的区别

阻塞是指:当一个事务请求一个资源尝试获取锁时,被其它事务锁定,请求的事务会一直等待,直到其它事务把该锁释放,这就发生了阻塞,默认情况SQLServer会一直等下去。所以阻塞往往能持续很长时间,这对程序的并发性能影响很大。

死锁是两个或多个进程之间的相互等待,一般在5秒就会检测出来,消除死锁。并发性能不像阻塞那么严重。

阻塞是单向的,互相阻塞就变成了死锁。

6.3 尽量避免死锁的方法

按同一顺序访问对象

避免事务中的用户交互

保持事务简短

合理使用隔离级别

调整语句的执行计划,减少锁的申请数目。

七.事务并发检查

在检查并发方面,有很多种方式像原来的如sp_who,sp_who2等系统存储过程,perfmon计数器,sql Trace/profiler工具等,检测和分析并发问题,还包括sql server 2005以及以上的:

DMV  特别是sys.dm_os_wait_stats和sys.dm_os_waiting_tasks ,这里简单讲下并发检查

例如:查询用户会话的相关信息

SELECT  blocking_session_id FROM sys.dm_os_waiting_tasks WHERE session_id>50

blocking_session_id 阻塞会话值有时为负数:

-2 :被阻塞资源属于孤立分布式事务。

-3: 被阻塞资源属于递延恢复事务。

-4: 对于锁存器等待,内锁存器状态转换阻止了session的识别。

例如:下面查询阻塞超5秒的等待

SELECT blocking_session_id FROM sys.dm_os_waiting_tasks WHERE wait_duration_ms>5000

例如:只关注锁的阻塞,可以查看sys.dm_tran_locks
SELECT * FROM sys.dm_tran_locks WHERE request_status=’wait’

通过sys.dm_exec_requests查看用户请求

通过sqlDiag.exe收集运行系统的信息

通过errorlog里打开跟踪标识1222 来分析死锁

通过sys.sysprocess 检测阻塞。

sql server 锁与事务拨云见日(中) - 花阴偷移 - 博客园

mikel阅读(448)

来源: sql server 锁与事务拨云见日(中) – 花阴偷移 – 博客园

一.事务的概述

上一章节里,重点讲到了锁,以及锁与事务的关系。离上篇发布时间好几天了,每天利用一点空闲时间还真是要坚持。听《明朝那些事儿》中讲到”人与人最小的差距是聪明,人与人最大的差距是坚持”很经典的一句话一直记得。这篇重点围绕事务来开展。涉及的知识点包括:事务的概述,事务并发控制模型,并发产生的负面影响,事务隔离级别以及不同的表现。本章多以文字描述为主,没有多少代码量,重点是阐述不同隔离级别的不同表现,在以后的业务中,涉及到事务时,本文可以用来做个参考。

1.1 事务ACID

事务作为一个逻辑工作单元执行一系列的操作,它包括四个属性:原子性、一致性、隔离性和持久性 (ACID) 属性, 只有这样才能成为一个事务。

原子性:当一个事务被当作一个单独的工作单元时,不管事务内有什么,都是一个整体。对于其数据修改,要么全都执行,要么全都不执行。

一致性:事务在完成时,必须使所有的数据都保持一个逻辑一致状态。

隔离性:并发事务所做的修改必须与其他并发事务所做的修改隔离。 事务能识别数据所处的状态,要么是另一并发事务修改它之前的状态,要么是并发事务修改它之后的状态。

持久性:一但事务完全,它的效果是永久存于系统的。该修改即使出现系统故障也将一直保持。 SQL Server 2014和更高版本启用延迟的持久事务。

1.2 事务的操作模式有几下几种:

自动提交事务:每条单独的语句都是一个事务。

显式事务:每个事务均以 BEGIN TRANSACTION 语句显式开始,以 COMMIT 或 ROLLBACK 语句显式结束。

隐式事务:在前一个事务完成时新事务隐式启动,但每个事务仍以 COMMIT 或 ROLLBACK 语句显式完成。

批处理级事务:只能应用于多个活动结果集 (MARS),在 MARS 会话中启动的 Transact-SQL 显式或隐式事务变为批处理级事务。在SQL server 2000 必须对每个 SqlCommand 对象使用独立的 SqlConnection 对象。但是 SQL Server 2005 启用了 MARS,可以共用一个SqlConnection 对象。

本章重点讲到显式事务的隔离级别

二. 事务并发模型

2.1 并发访问是指:多用户同时访问一种资源被视为并发访问资源。 并发数据访问需要某些机制,以防止多个用户试图修改其他用户正在使用的资源时产生负面影响,机制就是下面讲的事务隔离级别。处于活动状态而不互相干涉的并发用户数据越多,并发性就越好。当一个正在修改数据的用户阻止了其他用户读取数据,或者当一个正在读取数据的用户阻止了其它用户修改数据时,并发性就降低了。

2.2 并发类型

SQLServer里数据库系统可以采用两种方式来管理并发数据访问:乐观并发控制和悲观并发控制,在sql server 2000以前只有悲观并发。乐观并发控制是一种称为行版本控制(row versioning)的技术支持。这二种技术并发控制的区别在于:是在冲突发生前进行防止,还是在发生后采用某种方法来处理冲突。

悲观并发控制

在悲观并发中,sql server是获取锁来阻塞对于其它用户正在使用数据的访问。  用户操作的读与写之间是会互相阻塞的。

乐观并发控制

乐观并发控制默认采用行版本控制使其它用户能够看到修改操作发生以前的数据状态,旧版本数据行会保存下来。因些读取数据不会受到其它用户正对该数据进行修改操作的影响,换言之修改数据不会受到其它用户正对该数据进行读取影响。 因为读取用户访问的数据行是一个被保存过的版本。  用户读与写之间不会互相阻塞,但写与写还是会发生阻塞。

2.3  事务并发带来的负面影响

修改数据的用户会影响同时读取或修改相同数据的其他用户。 即这些用户可以并发访问数据。 如果数据存储系统没有并发控制,则用户可能会看到以下负面影响:

并发影响 定义
丢失更新        当两个或多个事务选择同一行,然后基于最初选定的值更新该行时,会发生丢失更新问题。 每个事务都不知道其他事务的存在。   最后的更新 将覆盖由其他事务所做的更新,这将导致数据丢失。
脏读

 当一个用户修改了数据但尚未提交修改,而另一个正在读取的用户会读到这个修改从而导致不一致的状态发生。

不可重复读

一个用户在同一个事务中分别以两个读操作间隔读取相同资源时可能会得到不同的值。

虚拟读取(幻影)

一个事务里执行两个相同的查询,但第二个查询返回的行集合是不同的,此时就会发生虚拟读取。这种情况发生在where 查询中,比如 where count(1)<10。  同一个事务中多次使用相同的条件查询,select操作返回不同数据的结果集。

三.事务隔离级别

在sql server 2005及以上 支持五种隔离级别来控制“读”操作的行为,其中有三个是悲观并发模式,一个是乐观并发模式,剩下一个存在两种模式。 下面介绍隔离级别从允许的并发负作用(例如脏读或虚拟读取)的角度进行描述。

隔离级别  定义
未提交读
READUNCOMMITTED
 隔离事务的最低级别,未提交读不会发出共享锁,允许脏读,一个事务可能看见其他事务所做的尚未提交的更改。未提交读不会发出共享锁. 该项的作用与与SELECT表上加NOLOCK相同。
 

已提交读
READ COMMITTED

 一个事务不能读取其它事务修改但未提交的数据,避免了脏读。事务内语句运行完后便会释放共享锁,而不是等到事务提交的时候。 这是数据库引擎默认级别。
可重复读
REPEATABLE READ
 事务内查询语句运行完后不会释放共享锁,而是等到事务提交后.其它事务不能修改,删除,但可以插入新数据。
因为不是范围锁,可能发生虚拟读取
 可序列化SERIALIZABLE  隔离事务的最高级别,事务之间完全隔离。 阻止其它事务删除或插入任何行。 相当于SELECT上加HOLDLOCK相同, SELECT 操作使用 WHERE 子句时获取范围锁,主要为了避免虚拟读取
已提交读 快照隔离
READ COMMITTED SNAPSHOT ISOLATION level (RCSI)
当 READ_COMMITTED_SNAPSHOT 数据库选项设置为 ON 时,已提交读隔离使用行版本控制提供语句级读取一致性。 读取操作只需要 SCH-S 表级别的锁,不需要页锁或行锁。 使用行版本控制为每个语句提供一个在事务上一致的数据快照,因为该数据在语句开始时就存在。
快照隔离
SNAPSHOT ISOLATION level
(SI)
 快照隔离级别使用行版本控制来提供事务级别的读取一致性。 读取操作不获取页锁或行锁,只获取 SCH-S 表锁。 读取其他事务修改的行时,读取操作将检索启动事务时存在的行的版本。 当 ALLOW_SNAPSHOT_ISOLATION 数据库选项设置为 ON 时,只能对数据库使用快照隔离。 默认情况下,用户数据库的此选项设置为 OFF。

sql server主要是通过共享锁申请和释放机制的不同处理,来实现不同的事务隔离级别。不同隔离级别允许的并发副作用如下:

隔离级别 脏读 不可重复读 幻影读 并发控制模型
 未提交读 悲观
 已提交读 悲观
 已提交读快照 乐观
 可重复读 悲观
 快照 乐观
可串行化 悲观

不同隔离级别对共享锁的不同处理方式如下:

隔离级别 是否申请共享锁 何时释放 有无范围锁
未提交读
已提交读 当前语句做完时
可重复读 事务提交时
可序列化 事务提交时

四.事务隔离不同表现

   设置未提交读

SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED

设置提交读

SET TRANSACTION ISOLATION LEVEL READ COMMITTED

设置可重复读

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

4.1 未提交读和提交读与其它事务并发,的区别如下表格:

未提交读 提交读 其它事务
SELECT Model FROM Product

WHERE SID=10905

显示model 值为test

SELECT Model FROM Product

WHERE SID=10905

显示model 值为test

begin  tran

update  product set model=’test1′

where SID=10905

SET TRANSACTION ISOLATION
LEVEL READ UNCOMMITTED
SET TRANSACTION ISOLATION

LEVEL READ COMMITTED

 这个事务将model值改为test1.

此时修改的X锁未释放

SELECT Model FROM Product

WHERE SID=10905

显示model值为test1,但这并不正确,

因为其它事务还没有提交。没有获取共享锁

SELECT Model FROM Product

WHERE SID=10905

查询被阻塞

申请获取共享锁时失败,因为X锁未释放

 
阻塞消失,得到的值还是test  rollback tran

这里事务回滚了x锁释放,值还是test

4.2  已提交读和可重复读与其它事务并发,的区别如下表格:

已提交读 可重复读 其它事务
SET TRANSACTION ISOLATION
LEVEL READ UNCOMMITTED
begin tran
SELECT Model FROM
ProductWHERE SID=10905
第一次查询显示model值为 test
SET TRANSACTION ISOLATION
LEVEL REPEATABLE READ
begin tran
SELECT Model FROM Product
WHERE SID=10905
第一次查询显示model值为 test
 
begin tran
update product set model=’test1′
where SID=10905
将model值改为 test1

另一事务是已提交读时,这里事务修改成功
提交读共享锁查询后就释放。

另一事务是可重复读时,这里事务修改阻塞
可重复读共享锁一直保留到事务提交

SELECT Model FROM Product
WHERE SID=10905
第二次查询值显示为 test1
SELECT Model FROM Product
WHERE SID=10905
第二次查询显示值显示为 test
commit tran

这里就是一个事务里多次读取同一值
结果可能不一致

  commit tran

未完…sql server 锁与事务拨云见日(下)