视图后面加with(nolock)后

本文通过实验探讨在SQL报表生成过程中,使用with(nolock)对视图进行无锁操作的效果,并分析其对数据一致性和业务系统的影响。

原文出处:http://www.cnblogs.com/ASPNET2008/archive/2012/02/09/2343538.html

      一些小的企业项目,往往存在很多局限性,这里讲的局限性是指这些项目在面对某些需求时,所选用的解决方案不多。就拿我目前处理这个来讲吧,说的通俗点就是为业务部门生成相关报表。我目前理解的报表生成方式,总结如下:
      
      第一:在业务库上执行SQL语句或者存储过程,实时生成数据。这其中又分为三种:
      
       1:在主库上操作,及业务系统操作的数据库与生成报表的SQL逻辑同处一个数据库。
       2: 在主库的只读库即从库上操作,一般可以通过订阅复制来设置主从库,产生报表的逻辑只操作从库。
       3: 主库定时生成一个备份库,报表从生成的备份库中操作。
       
      第二:由数据仓库生成相应报表。
          这部分在这里就不多说了。
          
      我目前的项目就属于第一种的第一小分类,即直接在主库上以执行SQL方式产生数据,其中的原因也不用多说,低成本。但由于生成报表的需要的时间一般都较长,容易对业务表进行长时间的锁定,影响业务系统的正常使用,反过来,如果用户正在操作业务库,此时如果同时执行报表生成,就会形成锁等待。为此决定对报表相关查询表进行无锁操作,即在表后面加 with(nolock),但我们为客户端是以视图形式提供,为此产生一个问题:在视图后面加with(nolock),是否意味着视图内的所有表都是nolock?
      
      我在网上没找到相关内容,于是还是眼见为实,决定做个小实验。重点需要搞清楚如下几个问题:
      
      问题一:如何人为造成一个锁?即让某个表被某个线程长时间锁定。


      我们可以BEGIN TRAN mytran,然后操作一个表,比如update,但不commit tran,此时被操作的表就会被当前线程长时间锁定。我们可以用sp_who2来查询锁定情况,如下图所示,第一个框代表状态,第二个框是CPUTime。还可以利用sp_lock查看锁的类型。


      
      问题二:如何制造脏读的情况,我们都说加了nolock,容易形成脏读,但脏读到底怎么样呢?
      
      1:创建一个测试用的表:
      

复制代码
-- 创建表
CREATE  TABLE mytest
    (
      id  INT  IDENTITY
              NOT  NULL ,
      data  UNIQUEIDENTIFIER  DEFAULT (  NEWID() )
                             NOT  NULL
    )
复制代码

 

    这里需要创建一个聚焦索引,如果不创建此聚焦索引,后续的结果会发生变化。
    

-- 创建索引
CREATE  UNIQUE  CLUSTERED  INDEX cidx  ON mytest( data )

 

     2:为表添加一些测试数据。
   

复制代码
-- 添加数据
DECLARE  @i  INT ;
SET  @i  =  1 ;
WHILE  @i  <=  50000 
     BEGIN
         INSERT  mytest
                 DEFAULT  VALUES  
         SET  @i  =  @i  +  1
     END
复制代码

    

     3:创建测试视图:
    

CREATE  VIEW view_mytest
AS
   SELECT  *  FROM mytest
go

 

    4:创建一个嵌套视图,嵌套视图的目的就是想看看在最上层视图上加nolock,是否会传递到最底层的基表。
   

CREATE  VIEW view_mytest2
AS
   SELECT a. *  FROM view_mytest a
   LEFT  JOIN dbo.Report b
   ON a.id =b.id
go

 

     5:在MSMS中打开两个窗口:
      窗口一:执行如下脚本,提交一个事务,更新测试表,但不提交,形成长时间锁定。
      

BEGIN  TRAN mytran
UPDATE  mytest
SET     data  =  NEWID()

 

      窗口二:对测试表进行行数统计,当发现数据出现错误时,打印出提示信息。      

DECLARE @totalrows INT ,

    @currentnow INT ,

    @errorcount TINYINT

SET @errorcount = 0

SELECT  @totalrows = COUNT(1)

FROM    mytest 

WHILE 1 = 1 

    BEGIN

        SELECT  @currentnow = COUNT(1)

        FROM    view_mytest2 WITH ( NOLOCK )  

        IF @totalrows <> @currentnow 

            BEGIN

                PRINT '查询总数= ' + CAST(@currentnow AS VARCHAR(10)) + ' 差异数量= '

                    + CAST(@currentnow - @totalrows AS VARCHAR(10))

                SET @errorcount = @errorcount + 1

                IF @errorcount >= 8 

                    BREAK

            END

    END

      6:查看窗口二,如果nolock,理论上应该会出现错误提示信息,出现此错误提示信息就是因为在无锁情况下有可能出现多读或者是少读的现象。


    

    

      上图表示,在视图上添加nolock跟在基表后加nolock效果一样,只不过在视图上加就相当于视图内所有表都无锁,这样不太灵活,尽管有脏读现象,但我们的业务逻辑一般都是多表相匹配后的情况,如果一个事务中会操作多个表,生成报表时,某些表形成了脏读,也不会影响最终数据,如果所有表都操作完毕,还未提交事务,此时虽然查询出的数据可能是错误数据,但属于可接受范围之内。

  

      文中若不错误地方望批评指正。 

   

      参考资料:http://www.dotblogs.com.tw/ricochen/archive/2011/04/15/22758.aspx 

<think>我们正在使用MyBatis-Plus的LambdaQueryWrapper,并且希望在生成的SQL语句中,在表名后面WITH(NOLOCK)提示(通常用于SQL Server,表示在读取数据时不共享锁)。 在MyBatis-Plus中,我们可以通过自定义SQL片段或者在Wrapper中直接添表提示(table hint)来实现。 方法一:使用last方法追SQL片段 我们可以通过last方法在SQL语句的最后添WITH(NOLOCK),但是注意,这样可能会将WITH(NOLOCK)到整个查询的末尾,而不是表名后面。 例如:wrapper.last("WITH(NOLOCK)") 会生成类似:SELECT ... FROM table ... WITH(NOLOCK) 这并不符合我们的要求(我们希望在表名后面)。 方法二:使用自定义表名(通过注解或XML) 我们可以在实体类的@TableName注解中直接添表提示,例如: @TableName(value = "table_name", autoResultMap = true) 但是这样写无法动态添,且MyBatis-Plus目前不支持在注解中直接添WITH(NOLOCK)。 方法三:使用自定义SQL(在XML中写SQL,并添表提示) 这是最直接的方法,但这样我们就不能使用LambdaQueryWrapper的链式调用了。 方法四:使用Interceptor(拦截器)修改SQL 我们可以通过MyBatis的插件机制,拦截SQL语句,然后在表名后面WITH(NOLOCK)。这种方法较为复杂,但可以全局生效。 方法五:在LambdaQueryWrapper中,我们可以使用自定义的SQL片段,但是需要自己拼接表名和WITH(NOLOCK)。然而,LambdaQueryWrapper本身没有提供直接的方法。 实际上,MyBatis-Plus从3.0.7版本开始,提供了@SqlParser注解(后来改为@InterceptorIgnore)来忽略某些拦截器,但也没有直接提供表提示。 在最新版本的MyBatis-Plus(3.4.0以上)中,提供了新的方式:使用TableNameParser处理表名,我们可以通过实现TableNameHandler接口来动态修改表名,但是这种方式也不能直接添WITH(NOLOCK)。 考虑到这些,我们可以采用以下两种方式: 1. 通过last方法添整个查询的WITH(NOLOCK)(适用于SQL Server,因为WITH(NOLOCK)可以放在查询的末尾,但请注意,这种方法可能不是最佳实践,因为WITH(NOLOCK)通常紧跟在表名后,并且如果查询中有多个表则无法指定) 2. 使用自定义SQL(在XML中写SQL,然后使用QueryWrapper,但这样就会失去Lambda表达式的好处) 然而,我发现MyBatis-Plus官方并没有提供直接在表名后添表提示的功能。因此,我们可以考虑使用自定义拦截器。 下面是一个通过拦截器在每个表名后面WITH(NOLOCK)的示例: ```java @Intercepts({@Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})}) public class TableLockInterceptor implements Interceptor { @Override public Object intercept(Invocation invocation) throws Throwable { StatementHandler statementHandler = (StatementHandler) invocation.getTarget(); MetaObject metaObject = SystemMetaObject.forObject(statementHandler); // 获取原始SQL String originalSql = (String) metaObject.getValue("delegate.boundSql.sql"); // 替换表名为 表名 WITH(NOLOCK) // 注意:正则替换需要谨慎,这里假设表名在FROM和JOIN后面出现 String modifiedSql = originalSql.replaceAll("(?i)(from|join)\\s+(\\w+)\\b", "$1 $2 WITH(NOLOCK)"); metaObject.setValue("delegate.boundSql.sql", modifiedSql); return invocation.proceed(); } } ``` 但是请注意,这种方法可能会错误地替换一些不应该替换的地方(例如子查询中的表名),而且正则表达式需要根据实际情况调整。 另外,在MyBatis-Plus的LambdaQueryWrapper中,如果我们知道表名,我们也可以尝试在构造查询时直接指定表名(带WITH(NOLOCK)),例如: ```java LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.setEntityClass(User.class); wrapper.setSqlSelect("id,name"); // 可以选择需要的字段 wrapper.getCustomSqlSegment().setTableName("user WITH(NOLOCK)"); ``` 但是,查看LambdaQueryWrapper的源码,它并没有提供直接设置表名的方法。QueryWrapper提供了fromSql方法,但LambdaQueryWrapper没有。 因此,我建议使用以下方式: 使用QueryWrapper代替LambdaQueryWrapper,然后使用fromSql方法: ```java QueryWrapper<User> wrapper = new QueryWrapper<>(); wrapper.from("user WITH(NOLOCK)"); wrapper.select("id", "name"); wrapper.eq("age", 18); ``` 但是这样我们就不能使用Lambda表达式了。 总结:在LambdaQueryWrapper中直接添表名后面nolock是不容易的。如果必须使用Lambda表达式,我们可以考虑使用拦截器全局添(但要小心),或者使用QueryWrapper并放弃Lambda表达式。 另外,MyBatis-Plus在3.4.3.4版本中新增了tableHint方法用于表提示(目前仅支持MySQL的USE INDEX等,但我们可以尝试用它来添SQL Server的WITH(NOLOCK))。 查看源码,在com.baomidou.mybatisplus.core.conditions.AbstractWrapper中,有tableHint方法: ```java public Children tableHint(String tableHint) { if (StringUtils.isNotBlank(tableHint)) { this.tableNames.setTableHints(tableHint); } return typedThis; } ``` 我们可以这样使用: ```java LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.tableHint("WITH(NOLOCK)"); // 然后继续使用条件 wrapper.eq(User::getName, "test"); ``` 但是,注意查看源码,这个方法是在3.4.3.4版本入的,并且它生成的SQL片段是放在表名后面吗? 生成的SQL片段在源码中:com.baomidou.mybatisplus.core.conditions.segments.TableSegment#getSqlSegment 它会在表名后面上空格上我们设置的tableHint。 所以,我们可以这样使用: ```java wrapper.tableHint("WITH(NOLOCK)"); ``` 但是注意:这个方法是在3.4.3.4版本才入的。如果你的MyBatis-Plus版本低于这个,需要升级。 因此,如果你使用的是3.4.3.4及以上版本,推荐使用`tableHint`方法。 示例代码: ```java LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.tableHint("WITH(NOLOCK)") .eq(User::getAge, 20); ``` 生成的SQL:SELECT ... FROM user WITH(NOLOCK) WHERE ... 注意:如果查询中涉及多个表,那么tableHint会作用于主表(即我们查询的那个实体类对应的表)。如果需要对其他表也,那么可能需要使用其他方式。 所以,根据你的需求,如果你使用的是较新版本的MyBatis-Plus(>=3.4.3.4),请使用`tableHint`方法。否则,考虑升级或者使用其他方法。 相关问题: 1. 如何升级MyBatis-Plus到最新版本? 2. 在MyBatis-Plus中,除了tableHint方法,还有哪些方式可以影响生成的SQL? 3. 使用WITH(NOLOCK)有哪些注意事项? </think>在 MyBatis-Plus 的 `LambdaQueryWrapper` 中,要为表名添 `WITH(NOLOCK)` 提示(主要用于 SQL Server 的脏读场景),可通过以下方法实现: ### 1. **使用 `last()` 方法追 SQL 片段** ```java LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getStatus, 1) .last("WITH(NOLOCK)"); // 在 SQL 末尾追 ``` **效果**: ```sql SELECT ... FROM user WHERE status = 1 WITH(NOLOCK) ``` **注意**:此方法会将 `WITH(NOLOCK)` 追到整个 SQL 语句末尾,而非紧贴表名。 --- ### 2. **结合 `@SqlParser` 注解(全局表提示)** 在实体类上声明表级锁提示: ```java @TableName(value = "user", autoResultMap = true) @SqlParser(filter = true, blockAttack = false) // 开启自定义 SQL 解析 public class User { // 字段定义... } ``` 然后在 XML 或自定义 SQL 中直接写: ```sql SELECT * FROM user WITH(NOLOCK) WHERE ... ``` **优点**:语义明确,直接作用于表名。 --- ### 3. **自定义 SQL 拦截器(动态注入)** 通过 MyBatis 拦截器动态修改 SQL: ```java @Intercepts({@Signature(type = StatementHandler.class, method =prepare", args = {Connection.class, Integer.class})}) public class NolockInterceptor implements Interceptor { @Override public Object intercept(Invocation iv) throws Throwable { StatementHandler handler = (StatementHandler) iv.getTarget(); String sql = handler.getBoundSql().getSql(); sql = sql.replaceAll("(?i)FROM\\s+(\\w+)", "FROM $1 WITH(NOLOCK)"); // 正则匹配表名 Field field = handler.getBoundSql().getClass().getDeclaredField("sql"); field.setAccessible(true); field.set(handler.getBoundSql(), sql); return iv.proceed(); } } ``` **注册拦截器**: ```java @Bean public NolockInterceptor nolockInterceptor() { return new NolockInterceptor(); } ``` **效果**:自动将所有 SQL 的表名后追 `WITH(NOLOCK)`。 --- ### ️ 重要注意事项 1. **数据库兼容性** `WITH(NOLOCK)` 仅适用于 SQL Server,MySQL 需用 `LOCK IN SHARE MODE`,Oracle 用 `/*+ NOLOCK */` 语法。 2. **脏读风险** 使用 `NOLOCK` 可能导致读取未提交数据,适用于对数据一致性要求不高的场景[^1]。 3. **性能影响** 虽然减少锁竞争,但可能因脏读导致业务逻辑错误。 --- ### 最佳实践建议 ```java // 明确指定表名+提示(需自定义SQL) @Select("SELECT * FROM user WITH(NOLOCK) WHERE status = #{status}") List<User> selectUsersWithNolock(@Param("status") int status); ``` **推荐场景**: - 高频统计报表(容忍少量脏读) - 历史数据查询(无实时更新) ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值