哎,说到NOLOCK,我就上火。上个月我们生产库跑一个报表,开发那边图省事,给查询加了 WITH(NOLOCK)。人跟我说“反正就是读个大概,不影响”。结果当天下午老板拿报表和财务系统对账,差了十几万——不是多几块零钱,是十几万!后来我查了半天,发现就是那个NOLOCK在搞鬼——它读到了一条订单在插入过程中只更新了一半的数据,状态是“已支付”,但金额字段还没写进去,刚好被那个报表扫到了,就出了大问题。最后是把那个查询改成快照隔离级别才算完,但那个下午的狼狈,真不想再来第二次。
你可能会问,NOLOCK不就是不加锁吗?能有多乱?实际上,NOLOCK读的是正在修改中的数据页,等于你把头伸进了一个正在装修的房间里,看到一半柜子、一半墙纸,你觉得你看到了全貌,但实际是拼凑出来的。SQL Server的默认隔离级别READ COMMITTED读到的是已提交数据,而NOLOCK(对应READ UNCOMMITTED)连未提交的都能读。相比之下,快照隔离级别(SNAPSHOT ISOLATION)读的是历史版本,每次查询看到的是一个一致性的快照,不会看到半拉子数据。但要注意,启用快照隔离需要两步:先改数据库设置ALLOW_SNAPSHOT_ISOLATION ON,再把会话隔离级别改成SNAPSHOT。另外快照隔离会把版本存到tempdb,对tempdb空间有压力,而且如果更新很频繁,版本链会很长,查询反而会变慢。网上那些教程,基本没人告诉你这一层。
确实,当年内存和硬盘都贵,锁竞争是性能的大敌,NOLOCK成了“优化神器”。但现在都是SSD了,内存也动不动几十个G,普通查询走NOLOCK和走默认隔离级别,在没有任何阻塞的情况下,性能差距往往不到5%——我拿我们的订单表(两千多万行)做过测试,同样的范围扫描,NOLOCK花了2.1秒,默认读提交花了2.2秒,这0.1秒的差距你根本感觉不出来。只有在另一个事务正在更新同一批数据时,NOLOCK才会让你避免了等待,但代价就是读到不一致的中间状态。我还见过更尴尬的情况:一个报表用了NOLOCK读了一个正在跑的批量更新业务,结果同一个查询在不同时间跑了两次,返回的行数都不一样,开发来问我数据是不是有毛病,我能说什么?还不是自己选的NOLOCK。
我的观点可能和很多人不一样:在绝大多数业务系统里,NOLOCK根本不该用。数据库的核心职责是把数据“存对”,所有读操作都应该优先保证一致性。如果确实有报表要跑,又怕阻塞业务,用AlwaysOn的只读路由或者单独建一个同步的日志备份库,既能保证你读到的是完整的、一致的数据,又不影响生产写入。如果连这些基础设施都没有,那就退一步用快照隔离,把脏读的隐患堵死。只有一种情况我可能会考虑NOLOCK——比如那种纯历史数据只读表,本身就没有写事务,或者在做DBA调优时临时看一下当前锁等待的数据分布,用完立刻关掉。但你真要在报表系统里长期骑这匹野马,那就得做好哪天数据对不上、背锅到底的心理准备。反正我是怕了,劝你慎用。