updlock实例分析

先给出一个场景,然后围绕这个场景展开讨论。
场景:商品限购,商家在发布活动时,要求活动内的每个商品每位用户限购一个。

假设现在有一张表活动订单表,记录了用户参与活动的情况。
活动订单表(主键,活动主键,用户主键,商品主键)

-- sql server 2008
CREATE TABLE [dbo].[tbl_activity_order] (
  [id] bigint NOT NULL,
  [activity_id] bigint NOT NULL,
  [user_id] bigint NOT NULL,
  [goods_id] bigint NOT NULL,
  PRIMARY KEY CLUSTERED ([id])
WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON)
)
GO

EXEC sp_addextendedproperty
'MS_Description', N'活动主键',
'SCHEMA', N'dbo',
'TABLE', N'Untitled',
'COLUMN', N'activity_id'
GO

EXEC sp_addextendedproperty
'MS_Description', N'用户主键',
'SCHEMA', N'dbo',
'TABLE', N'Untitled',
'COLUMN', N'user_id'
GO

EXEC sp_addextendedproperty
'MS_Description', N'商品主键',
'SCHEMA', N'dbo',
'TABLE', N'Untitled',
'COLUMN', N'goods_id'

拿到了表结构,下面我们就开始想如何实现限购这个功能吧。最先进入脑海的应该就是在用户下单前先判断下用户是否有过购买记录,如果已经有了购买记录,则触发限购,反之可以正常购买。

步骤事务1
1BEGIN TRANSACTION;
2select count(*) from tbl_activity_order where activity_id = ? and user_id = ? and goods_id = ?;
3insert into tbl_activity_order(id, activity_id, user_id, goods_id) values(?, ?, ?, ?);

仔细想想是不是这样的流程就能够保证用户不会超买的问题呢?
现在我们开启两个事务来看看吧

操作步骤事务1事务2
1BEGIN TRANSACTION;BEGIN TRANSACTION;
2select count(*) from tbl_activity_order where activity_id = 1 and user_id = 1 and goods_id = 1;select count(*) from tbl_activity_order where activity_id = 1 and user_id = 1 and goods_id = 1;
3insert into tbl_activity_order(id, activity_id, user_id, goods_id) values(1, 1, 1, 1);
4insert into tbl_activity_order(id, activity_id, user_id, goods_id) values(2, 1, 1, 1);
5commit;
6commit;

当我们执行完上面这个步骤之后,我们会发现tbl_activity_order表中多了两条记录,且这两条记录的activity_id,user_id,goods_id是一样的,说明限购失效了。有人会说我加个唯一索引idx_activity_user_goods不就可以了吗?保证数据库中这三个字段的组合不会出现重复。想法是不错,可以如果下次活动限购不是1个,而是2个或者更多怎么办,那就允许用户多次下单了,唯一索引就不可取了。

问1:想想为什么上面的步骤不能满足限购?
         因为两个事务的执行步骤出现了交叉。

问2:那么怎样才能防止此类现象的出现呢?
         不让事务在此处的执行出现交叉。

问3:那么怎样才能不让事务在此处执行出现交叉呢?
         如果我们能够在select的时候就锁住表是不是就好了。此时updlock就派上了用场。

问4:现在想想数据库在接收到select命令时,它是怎么查询到我们想要的结果的?
         我们知道关系型数据库一般都是使用树的方式存放索引的,此时你就需要在你的脑海里想象出一颗树。我们在建tbl_activity_order表时除了默认的主键索引外,并没有新建其他的索引。因此当我们执行

select count(*) from tbl_activity_order where activity_id = 1 and user_id = 1 and goods_id = 1;

语句时,使用的是聚集索引扫描,也就是说依次扫描聚集索引树上的每个叶子节点,然后从磁盘上把每个主键对应的记录值读出来,再判断activity_id,user_id,goods_id是否满足查找条件,如果满足条件结果值就+1,直到遍历到最后一个节点的最后一条记录才会把结果展示给我们(其实学过数据结构的都知道这个查找的过程就是在做树的遍历查找)。

问5:我们知道锁的类型大体上就两种:互斥锁、共享锁;锁粒度可分为:表锁、页锁、行锁。那么我们这里的select应该选择什么锁?
         因为要防止事务交叉,只能使用互斥锁。同时我们说要在实现功能的前提下使锁的粒度尽可能的小,因此这里如果可以我们应该选择行锁。

问6:我们怎么实现这个锁呢?
         在问题4中我们分析了select的执行步骤,知道了它是借助聚集索引实现的全表扫描。那么我们能够在现有条件下实现行锁呢?这个恐怕是无法实现的,因为聚集索引是按照id的顺序来存储的,所以全局来看只有id是有序的,其余字段都是无序的。我们可以执行下面的步骤验证下我们的猜想

-- 清空测试数据
delete from tbl_activity_order;

-- 插入两组活动数据
insert into tbl_activity_order(id, activity_id, user_id, goods_id) values
(1, 1, 1, 1),
(2, 1, 2, 1),
(3, 2, 1, 1),
(4, 2, 2, 1);
步骤事务1事务2
1BEGIN TRANSACTION;BEGIN TRANSACTION;
2select * from tbl_activity_order WITH(updlock, INDEX(PK__tbl_acti__3213E83F51900108)) where activity_id = 1 and user_id = 1 and goods_id = 1;
3select * from tbl_activity_order WITH(updlock, INDEX(PK__tbl_acti__3213E83F51900108)) where activity_id = 1 and user_id = 2 and goods_id = 1;
4commit;
5commit;

注:PK__tbl_acti__3213E83F51900108是聚集索引的名称,大家需要替换成自己的聚集索引去执行。

执行到步骤3时发现卡住了,必须把步骤4执行之后,步骤3才能放行。由于两个select的查找谓词不同,所以他们返回的行不同,如果这里是行锁的话,步骤3不应该被卡住,这里步骤3被卡住也就说明了这里并不是行锁。

现在我们新建一个索引idx_activity_user_goods,然后再使用新建的索引替换聚集索引

CREATE NONCLUSTERED INDEX [idx_activity_user_goods]
ON [dbo].[tbl_activity_order] (
  [activity_id],
  [user_id],
  [goods_id]
)
步骤事务1事务2
1BEGIN TRANSACTION;BEGIN TRANSACTION;
2select * from tbl_activity_order WITH(updlock, INDEX(idx_activity_user_goods)) where activity_id = 1 and user_id = 1 and goods_id = 1;
3select * from tbl_activity_order WITH(updlock, INDEX(idx_activity_user_goods)) where activity_id = 1 and user_id = 2 and goods_id = 1;
4commit;
5commit;

执行上述步骤发现并没有像使用聚集索引那样出现锁等待的情况。

现在我们已经解决了select加锁的问题,但是到现在都只是select操作,还没有进行update操作,下面我们还是使用2个事务来测试吧。

-- 清空测试数据
delete from tbl_activity_order;
步骤事务1事务2
1BEGIN TRANSACTION;BEGIN TRANSACTION;
2select * from tbl_activity_order WITH(updlock, INDEX(idx_activity_user_goods)) where activity_id = 1 and user_id = 1 and goods_id = 1;
3select * from tbl_activity_order WITH(updlock, INDEX(idx_activity_user_goods)) where activity_id = 1 and user_id = 1 and goods_id = 1;
4insert into tbl_activity_order(id, activity_id, user_id, goods_id) values(5, 1, 1, 1);
5commit;
6insert into tbl_activity_order(id, activity_id, user_id, goods_id) values(6, 1, 1, 1);
7commit;

执行上述步骤后发现我们会在步骤3卡住,然后执行步骤4/5之后,步骤3才会被放行,此时查询到表中已经存在一条记录,则跳过步骤6,执行步骤7。

至此限购的功能就此完成,现在回头看下我们的改动

  1. 增加一个索引idx_activity_user_goods
  2. 改写原先的select语句,加上WITH(updlock, INDEX(idx_activity_user_goods)) ,从而实现阻塞同一个用户在同一个活动下购买同一个商品的流程(注意这段话有三个同一个:同一个用户,同一个活动,同一个商品。这样能够保证我们锁粒度最小,对并发的影响降到最低。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
@Configuration public class ScheduleConfig { @Bean public SchedulerFactoryBean schedulerFactoryBean(DataSource dataSource) { SchedulerFactoryBean factory = new SchedulerFactoryBean(); factory.setDataSource(dataSource); // quartz参数 Properties prop = new Properties(); prop.put("org.quartz.scheduler.instanceName", "RuoyiScheduler"); prop.put("org.quartz.scheduler.instanceId", "AUTO"); // 线程池配置 prop.put("org.quartz.threadPool.class", "org.quartz.simpl.SimpleThreadPool"); prop.put("org.quartz.threadPool.threadCount", "20"); prop.put("org.quartz.threadPool.threadPriority", "5"); // JobStore配置 prop.put("org.quartz.jobStore.class", "org.quartz.impl.jdbcjobstore.JobStoreTX"); // 集群配置 prop.put("org.quartz.jobStore.isClustered", "true"); prop.put("org.quartz.jobStore.clusterCheckinInterval", "15000"); prop.put("org.quartz.jobStore.maxMisfiresToHandleAtATime", "1"); prop.put("org.quartz.jobStore.txIsolationLevelSerializable", "true"); // sqlserver 启用 // prop.put("org.quartz.jobStore.selectWithLockSQL", "SELECT * FROM {0}LOCKS UPDLOCK WHERE LOCK_NAME = ?"); prop.put("org.quartz.jobStore.misfireThreshold", "12000"); prop.put("org.quartz.jobStore.tablePrefix", "QRTZ_"); factory.setQuartzProperties(prop); factory.setSchedulerName("RuoyiScheduler"); // 延时启动 factory.setStartupDelay(1); factory.setApplicationContextSchedulerContextKey("applicationContextKey"); // 可选,QuartzScheduler // 启动时更新己存在的Job,这样就不用每次修改targetObject后删除qrtz_job_details表对应记录了 factory.setOverwriteExistingJobs(true); // 设置自动启动,默认为true factory.setAutoStartup(true); return factory; } } 什么意思
07-22

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值