使用悲观锁方式修复红包超发的bug

上一篇中使用ssm+mysql实现,存在并发超发问题,这里我们使用悲观锁的方式来解决这个逻辑错误,并验证数据一致性和性能状况。

超发问题分析

针对这个案例,用户抢到红包后,红包总量应-1,当多个用户同时抢红包,此时多个线程同时读得库存为n,相应的逻辑执行后,最后将均执update T_RED_PACKET set stock = stock - 1 where id = #{id} ,很明显这是错误的。

使用数据库锁的解决方案

使用悲观锁(排它锁 for update)

  1. 线程1在查询红包数时使用排他锁 select id, user_id as userId, amount, send_date as sendDate, total, unit_amount as unitAmount, stock, version, note from T_RED_PACKET where id = #{id} for update
  2. 然后进行后续的操作(redPacketDao.decreaseRedPacket 和 userRedPacketDao.grapRedPacket),更新红包数量,最后提交事务。
  3. 线程2在查询红包数时,如果线程1还未释放排他锁,它将等待
  4. 线程3同线程2,依次类推

使用乐观锁(依靠表的设计和代码)

  1. 在红包表添加version版本字段或者timestamp时间戳字段,这里我们使用version
  2. 线程1查询后,执行更新变成了update T_RED_PACKET set stock = stock - 1, version = version + 1 where id = #{id} and version = #{version}

这样,保证了修改的数据是和它查询出来的数据是一致的,而其他线程并未进行修改。当然,如果更新失败,表示在更新操作之前有其他线程已经更新了该红包数,那么就可以尝试重入机制来保证更新成功。

总结

  1. 悲观锁使用了排他锁,当程序独占锁时,其他程序就连查询都是不允许的,导致吞吐较低。如果在查询较多的情况下,可使用乐观锁。
  2. 乐观锁更新有可能会失败,甚至是更新几次都失败,这是有风险的。所以如果写入较频繁,对吞吐要求不高,可使用悲观锁。

悲观锁(抽象的描述,不真实存在这个锁)

悲观锁是在操作数据时,认为此操作会出现数据冲突,所以在进行每次操作时都要通过获取锁才能进行对相同数据的操作,所以悲观锁需要耗费较多的时间。另悲观锁是由数据库自己实现了的,使用的时候,直接调用数据库的相关语句即可。

由悲观锁涉及到的另外两个锁概念就出来了,它们就是共享锁与排它锁。共享锁和排它锁是悲观锁的不同的实现,它俩都属于悲观锁的范畴。

数据库的增删改操作默认都会加排他锁,而查询不会加任何锁。

共享锁(S锁)

共享锁指的就是对于多个不同的事务,对同一个资源共享同一个锁.

对某一资源加共享锁,自身可以读该资源,其他人也可以读该资源(也可以再继续加共享锁,即 共享锁可多个共存),但无法修改。要想修改就必须等所有共享锁都释放完之后.

语法:

select * from table lock in share mode ;

排他锁(X锁)

排它锁与共享锁相对应,就是指对于多个不同的事务,对同一个资源只能有一把锁。对某一资源加排他锁,自身可以进行增删改查,其他人无法进行任何操作。

与共享锁类型,在需要执行的语句后面加上for update就可以了

select * from table for update

修改代码

为了不影响上个版本,我们在mapper包下新增方法和和映射。 因为悲观锁是数据库提供的功能,所以仅仅在Dao层修改Sql,Service层无需新增新的接口,只需要切换下调用的Dao层的方法即可。

TRedPacketMapper.java

 //悲观锁获取红包信息
    TRedPacket getRedPacketForUpdate(Integer id);

TRedPacketMapper.xml

<!-- 悲观锁查询红包具体信息 -->
  <select id="getRedPacketForUpdate" resultMap="BaseResultMap" parameterType="java.lang.Integer" >
    select 
    <include refid="Base_Column_List" />
    from t_red_packet
    where id = #{id,jdbcType=INTEGER}
    for update
  </select>

直接在原来的映射sql片段后加for update就行

事务传播机制参考:
https://www.cnblogs.com/baizhanshi/p/10425467.html

对于共享锁大家可能很好理解,就是多个事务只能读数据不能改数据,对于排他锁大家的理解可能就有些差别,我当初就犯了一个错误,以为排他锁锁住一行数据后,其他事务就不能读取和修改该行数据,其实不是这样的。排他锁指的是一个事务在一行数据加上排他锁后,其他事务不能再在其上加其他的锁。mysql InnoDB引擎默认的修改数据语句,update,delete,insert都会自动给涉及到的数据加上排他锁,select语句默认不会加任何锁类型,如果加排他锁可以使用select ...for update语句,加共享锁可以使用select ... lock in share mode语句。所以加过排他锁的数据行在其他事务种是不能修改数据的,也不能通过for update和lock in share mode锁的方式查询数据,但可以直接通过select ...from...查询数据,因为普通查询没有任何锁机制。

悲观锁是一种利用数据库内部机制提供的锁的方法,也就是对更新的数据加锁,这样在并发期间一旦有一个事务持有了数据库记录的锁,其他的线程将不能再对数据进行更新.

当使用select ... for update ...where ...时,mysql进行row lock还是table lock只取决于是否能使用索引(例如主键,unique字段),能则为行锁,否则为表锁;未查到数据则无锁。而 使用'<>','like'等操作时,索引会失效,自然进行的是table lock

在 SQL 中加入的 for update 语句,意味着将持有对数据库记录的行更新锁(因为这里使用主键查询,所以只会对行加锁。如果使用的是非主键查询,要考虑是否对全表加锁的问题,加锁后可能引发其他查询的阻塞〉,那就意味着在高并发的场景下 , 当一条事务持有了这个更新锁才能往下操作,其他的线程如果要更新这条记录,都需要等待,这样就不会出现超发现象引发的数据一致性问题了.

Service层调用新的Dao方法

UserRedPacketServiceImpl.java

TRedPacket tRedPacket = tRedPacketMapper.getRedPacketForUpdate(redPacketId);

此时超发问题解决,但是用时反而减小了,奇怪。。。

mysql中update语句带索引为行锁,不带索引为表锁

update条件更新方法解决超发问题

条件更新update语句执行时会先锁住数据,然后再进行判断和更新,其他进程只能读,但是我们所有的访问请求都是update,将查询改变为条件判断,所以可以避免超发问题。

TRedPacketMapper.java

//update条件语句更新
    int updateByPanduan(Integer id);

TRedPacketMapper.xml

<update id="updateByPanduan">
        update t_red_packet set stock = if(stock>0,stock-1,stock) where id = 
        #{id,jdbcType=INTEGER}
    </update>

但是这样我们无法获知某次请求是否成功,因为正常mysql中update之后如果数据没有改变就会返回受影响的条数:0,但是mybatis中jdbc返回的是匹配的操作数,所以要修改一下。

db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/red_packet?useAffectedRows=true
jdbc.username=root
jdbc.password=suntong


http://www.cppcns.com/ruanjian/java/202830.html

此时记录条数正常,也没有超发,时间还可以接受。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容