2019独角兽企业重金招聘Python工程师标准>>>

背景

  在工作中接到一个需求:对于访问页面的前x名用户分发A奖品,x+1名及以后的用户分发另外一种奖品。在J2EE的开发中,我们知道servlet是单实例多线程的,Spring的Controller类也一样,所以这里需要考虑多线程并发时如何判断该用户是否为前x名。一种办法是在代码中用内存控制,例如添加一个成员变量,创建一个方法,并在内部使用synchronized块对该变量加锁,每次调用这个方法时,来一个用户就先判断变量是否大于x,小于的话就对该变量+1,直到该变量超过x为止。但是因为我们的代码是部署在多台服务器上的,而在多台服务器上同步内存比较麻烦,所以这种方法只适用于一台服务器的情况。另一种方法就是在数据库级别加锁,因为我们的数据库只有一个节点,所以只要在这一个节点上加了锁就可以控制来访的用户了。

mysql锁机制简介

  mysql提供了locking read机制,可以参考官方文档,一共有两种方式:SELECT ... FOR UPDATESELECT ... LOCK IN SHARE MODE。介绍它们之前,这里首先说一下X锁和S锁:

  • 若事务 T 对数据对象 A 加了 X 锁,则 T 就可以对 A 进行读取以及更新。在 T 释放 A 上的 X 锁以前,其它事务不能对 A 加任何类型的锁,但可以使用普通select语句获取值,而这个值不能保证是最新的,因为事务 T 可能修改了 A 的值,而它还没有提交;

  • 若事务 T 对数据对象 A 加了 S 锁,则 T 就可以对 A 进行读取,但不能进行更新。在 T 释放 A 上的 S 锁以前,其他事务可以再对 A 加 S 锁,但不能加 X 锁,从而可以读取 A ,但不能更新 A;

    SELECT ... FOR UPDATE是:

  为选择的行添加排它锁(X锁),保证查询到的数据是最新的数据,允许其它事务对该数据加上共享锁(S锁),但不能修改,只有当前事务可以修改,其它事务需要等当前事务commit或rollback之后才可以修改加锁的行;

SELECT ... LOCK IN SHARE MODE

  为选择的行添加共享锁(S锁),其它事务也可以对该行数据添加S锁,它保证了读取到的是最新的数据,并且不允许别人修改,但是自己也** 不一定 **能够修改,因为可能别的事务也对这个数据加了S锁;

实现

  从上面对mysql锁的介绍可以看到,我的业务需要不仅读的时候要阻止别人读最新值,而且还可能要修改读取后的结果,因此这里使用SELECT ... FOR UPDATE语句来控制用户访问的排名最合适。
  这里要注意一下,在mysql中用SELECT ... FOR UPDATE加锁,后面的WHERE条件是主键和非主键时有不同的加锁情况的,当WHERE后面是主键时,仅对行加锁,其它事务中可以对表的其他行进行增删改查,允许插入新的行;当WHERE后面的条件不是主键时,会锁全表,则其它事务不能对表的任意行进增删改的操作,插入新的行也不可以,只能查询。
  首先在数据库创建一个简单的表,结构如下:

列名 类型 备注
LOCK_KEY int 主键,每个锁是一行
LOCK_NUM int 当前排名,即代码中需要判断的变量x,初始值为0
LOCK_DESC varchar 锁的描述

  这个表中的每一行代表一个锁,也就是说下一次搞其它的活动,如果也需要对前x名进行控制,则插入一行记录用于代表一个锁。在java代码中,创建一个跟表映射的实体类LockBean,然后在DAO中添加两个方法,分别对应于查询和修改:

@Select(" select LOCK_KEY, LOCK_NUM, LOCK_DESC FROM LOCK_TABLE WHERE LOCK_KEY=#{lockKey} FOR UPDATE")
public LockBean findCurrentLock(int lockKey);@Update(" update LOCK_TABLE set LOCK_NUM = #{lockNum} where LOCK_KEY = #{lockKey}  ")
public void updateCurrentLock(LockBean lockBean);

  最后,在service层中添加事务控制,保证这两个DAO的方法在一个事务里面执行。需要注意的是,SELECT ... FOR UPDATE语句必须要关闭自动提交,例如使用普通的JDBC来调用,则需要先调用 connection.setAutocommit(flase)关闭自动commit操作,然后在selectupdate之后,再调用connection.commit()提交事务。如果想要在Navicat或mysql workbench中测试locking read功能,则需要先执行set autocommit=0语句关闭自动提交,然后再进行操作。

优化

  上面的方法对于每一次用户请求,都需要通过数据库级别的SELECT ... FOR UPDATE语句来加锁,可是往往前x名用户在总用户中所占的比例都是比较小的,毕竟大奖总是掌握在少数人手中嘛!如果每次都访问数据库,这样IO次数多了(同样也会导致网络请求次数增多,因为数据库只有一个节点)就会影响性能,所以我们在内存中再添加一个控制。在某个类中创建一个变量,用于判断前x名的奖品是否已经分发完毕:

public static volatile boolean isQueryNecessary = true;

  顺便复习一下,要使得volatile变量提供理想的线程安全,必须同时满足以下两个条件:

  1. 对变量的写操作不依赖于当前值
  2. 该变量没有bao含在具有其他变量的不变式中

  当变量声明为volatile后,所有线程对该对象的读取都会直接从主内存中获取,不会使用缓存的值,而在CPU缓存的一些值都会被标识为过期,从而完成线程对该对象的同步操作。具体介绍可见 Java 理论与实践: 正确使用 Volatile 变量.
  回归正题,在service层的处理方法giveAward()中,伪代码如下:

if(true == isQueryNecessary) {// 如果isQueryNecessary为真,则查询数据库,注意这里可能需要等待有X锁的线程释放锁LockBean bean = dao.findCurrentLock(lockKey);/** 判断bean中的lockNum是否>=x*  true :此时可能刚好等于x,也可能是在查询数据库时被别的线程抢先并更新了锁,*         即奖品别别人先抢完了,总之需要更新isQueryNecessary的值为false*         isQueryNecessary = false;*  false:lockNum++,*         dao.updateCurrentLock(bean); */
}if(false == isQueryNecessary) {// 再次判断是因为之前在查询数据库的时候有可能结果是lockNum >= x,// 导致isQueryNecessary的值被更新为false了// 总之这里处理x+1名以后的用户的逻辑logicForUserAfterX();
}

  这里对isQueryNecessary判断了两次,主要是因为在多线程抢资源的情况下,变量的值可能会在等待过程中改变,所以采用单例模式中DCL的思想,双重判断,从而确保对每个用户请求正确分流。
  通过这种优化后,对于单台服务器,顶多在第x个用户之后的部分请求(因为这些请求可能在抢第x个席位的过程中等待)会发生多于的数据库查询操作;而对于多台服务器,也只有部分的请求会执行多于的数据库查询,只要有一个请求在查询数据库之后发现已经不满足条件了就会把isQueryNecessary设为false,这台服务器后续的请求就不会再去查询数据库了,当全部的服务器上的isQueryNecessary都设为false之后,集群中后续的所有请求就都不再会查询数据库了,这样可以节省很多IO和网络操作。

redis实现方案

1. setnx方案

  redis的 setnx 命令可以用来实现分布式锁的功能,因此可以把奖品数量放到redis中,例如系统加载时从DB获取到奖品总数为80,则SET AWARDNUM 80,接下来每个请求线程中用setnx命令加分布式锁(具体实现可以参考网上的方案,思路是给一个常量设置值,即setnx constant value,value为随机值,设置可以的过期时间,这样只有当前线程能释放该分布式锁,若没有及时释放也可以等待锁过期后重新尝试获取),获取到分布式锁后,先判断奖品库存是否<=0,如是则同步更新内存变量,避免下次再查询redis;如果>0则表示秒杀成功,然后对该奖品数量减一,并释放分布式锁即可。

2. MQ方案

  该方案参考了这篇博文。redis有多种数据结构,例如链表,它可以作为一个MQ来使用,例如每个秒杀请求都放到队列中,再启动其它的线程去处理队列中前n个请求作为秒杀成功的处理。但是还有更简单的实现方案,例如系统初始化时从DB获取奖品数量为80,则初始化一个长度为80的list作为奖池,每个秒杀请求进来时使用LPOPRPOP命令从list中抽取一个奖品,如果返回值为空,则说明奖池已经空了,否则表示秒杀成功。因为redis命令执行的时候都是单线程的原子操作,所以该方案的好处是实现简单且不需要用分布式锁,感觉分布式锁可能会更耗时间,因为即要加锁又要更新奖品数量,而这个方案只要读一次redis就可以了。

转载于:https://my.oschina.net/JoeyXieIsCool/blog/664842

秒杀设计--mysql的锁机制应用和redis方案相关推荐

  1. MySQL调优(八):查缺补漏(mysql的锁机制,读写分离,执行计划详解,主从复制原理)

    mysql的锁机制 1.MySQL锁的基本介绍 ​ 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的 计算资源(如CPU.RAM.I/O等)的争用以外,数据也是一种供许多用户共 ...

  2. Mysql的锁机制之表锁

    Mysql的锁机制之表锁 锁是计算机协调多个进程或线程并发访问某一资源的机制. 在数据库中,除传统的计算资源(如CPU,RAM,I/O等)的争用外,数据也是一种供许多用户共享的资源,如何保证数据并发访 ...

  3. mysql默认锁机制是什么_MySQL中锁机制的原理是什么

    MySQL中锁机制的原理是什么 发布时间:2020-12-08 14:48:30 来源:亿速云 阅读:81 作者:Leah MySQL中锁机制的原理是什么?针对这个问题,这篇文章详细介绍了相对应的分析 ...

  4. MySQL InnoDB锁机制全面解析分享

    写在前面:在设计新零售供应链wms(仓库管理系统)库存模块时,为了防止并发情况对库存的影响,查阅了一些资料,对InnoDB锁机制有了更全面的了解,在此做出分享,如有疏漏望不吝指正,愿共同进步!(此篇为 ...

  5. 对比Oracle和Mysql在锁机制上的类似和差异点

    转:https://blog.csdn.net/c332472988/article/details/52804078 InnoDB行锁实现方式 InnoDB行锁是通过给索引上的索引项加锁来实现的,这 ...

  6. MySQL数据库锁机制之MyISAM引擎表锁和InnoDB行锁详解

    MySQL中的锁概念 Mysql中不同的存储引擎支持不同的锁机制.比如MyISAM和MEMORY存储引擎采用的表级锁,BDB采用的是页面锁,也支持表级锁,InnoDB存储引擎既支持行级锁,也支持表级锁 ...

  7. MySQL数据库——锁机制

    1 认识锁机制 在认识锁机制前,首先思考一个问题:在同一时刻,用户A和用户B同时要获取并修改sh_goods表中id等于2的stock库存量值,此时会发生什么呢? 假设在初始情况下,sh_ goods ...

  8. mysql的锁机制(读锁,写锁,表锁,行锁,悲观锁,乐观锁,间隙锁)

    读锁和写锁 介绍 MyISAM表锁中的读锁和写锁 读锁(共享锁S): 对同一个数据,多个读操作可以同时进行,互不干扰.加锁的会话只能对此表进行读操作,其他会话也只能进行读操作.MyISAM的读默认是加 ...

  9. 一文详解MySQL的锁机制

    一.表级锁.行级锁.页级锁 数据库锁定机制简单来说,就是数据库为了保证数据的一致性,而使各种共享资源在被并发访问变得有序所设计的一种规则. MySQL数据库由于其自身架构的特点,存在多种数据存储引擎, ...

最新文章

  1. 一口气发布1008种机器翻译模型,GitHub最火NLP项目大更新
  2. Cygwin,Nutch安装配置,检验是否正确(对网友守望者博客的修改---在此感谢守望者)1
  3. 活动目录从入门到精通
  4. 配置electron
  5. java 可插拔注解_20200311 8. 注解和可插拔性
  6. 按创建日期删除指定日期之前的文件夹及文件夹下的所有子目录
  7. post and get
  8. 广东省零售连锁协会执行会长:技术更新太快,消费者才是零售企业最大的对手...
  9. vue 创建项目之vue init webpack xxx
  10. 定时任务task:annotation-driven配置
  11. Molecule在GitHub与Gitee正式开源
  12. excel文件被写保护怎么解除_实用解除常用文件word、excel的写保护
  13. 北京周末去随便走走,体察千百年古城残存的
  14. P3406 海底高铁(前缀和+差分+坑点)
  15. mysql rds 主从_简单说说RDS的主从功能是如何实现
  16. canvas粒子线条插件使用
  17. 红旗linux hba卡wwn,Redhat Linux下安装HBA卡并查看WWN号
  18. 图表嵌入到数据表格下方_如何在excel图表下方添加数据表 如何在excel图表中显示数值...
  19. RIGOL示波器编程使用
  20. 一个服务器启动两个mysql实例

热门文章

  1. php ssh2函数,SSH2 函数 - PHP 7 中文文档
  2. cmd无法输出java结果_cmd中执行java命令没有输出结果
  3. linux下直接使用base64就可转换图片为二进制
  4. L1标准化与L2标准化
  5. android 动画多次使用,IOS使用animation引用同一个动画多次没反应,安卓可以
  6. mysql移植海思_minigui在海思解决方案(hi3515芯片)上的移植过程(转)
  7. php表格自动添加滚动条,jsp中为表格添加水平滚动条的方法
  8. python中解决中文乱码
  9. 自然语言处理期末复习(2)中文分词
  10. 信息系统项目管理师题型及题数