问题抛出

在近期的项目里面有一个功能是领取优惠券的功能,

问题描述:

每一个优惠券一共发行多少张,每个用户可以领取多少张:

如:A优惠券一共发行120张,每一个用户可以领取140张,当一个用户领取优惠券成功的时候,把领取的记录写入到另外一个表中(这张表我们暂且称为表B)

<!--减优惠券库存的SQL-->
<update id="reduceStock">update coupon set stock = stock - 1 where id = #{coupon_id}
</update>

上面的代码按照我们的逻辑是没有问题,我通过使用PostMan软件测试也是没有问题,但是上面的代码确实是有问题的。

**往往我们写的一些业务功能,在低并发的时候很多的问题会体现不出来。**所以这个领取优惠券的功能我通过Jmeter软件来进行压测。

这里配置了一下,大概会发送500次请求,那来验证下优惠券会不会出现超发的问题

执行结果,里面没有出现异常什么的,样本为500。包括在汇总报告里面也出现了一些返回信息是优惠券不足的信息,那来看下数据库里面的优惠券的总发行数量有没有变成负数呢?也就是有没有超发。

在测试的时候是测试的id为19的这条数据,测试完之后这里的总发行数量(stock)居然变成了-1(也就是超发了一张)。

问题引发

在解决这个问题之前,先来看下这个问题是如何引发出来的。

上面这张图是整个领取优惠券的流程(上图并没有使用流程图来画,我觉的这样画可能表达更清楚一些),在蓝色的框那里就是出现超扣减库存的时候。为啥这样说呢?

如果同时来了两个线程(你可以理解成是两个请求),比如先来的那个请求通过了检查(线程A),这时线程A还没有扣减库存,这时线程B经过一翻操作也通过了这个检查优惠券是否可领取的方法,然后线程A和线程B依次扣减库存或者是同时扣减库存,这样就会出现优惠券超领的情况。

清楚了问题引发的原因,那就来看看如何解决它们。

解决方案一(Java代码加锁)

在引起超发原因的那张图内可以看出,导致这一问题的根本原因是多个线程同时访问这个领取优惠券的方法,那只要保证在同一段只有一个线程进入到这个方法就可以了。

上面贴的代码就可以改成下面这样:

synchronized (this){LoginUser loginUser = LoginInterceptor.threadLocal.get();CouponDO couponDO = couponMapper.selectOne(new QueryWrapper<CouponDO>().eq("id", couponId).eq("category", categoryEnum.name()));if(couponDO == null){throw new BizException(BizCodeEnum.COUPON_NO_EXITS);}this.checkCoupon(couponDO,loginUser.getId());//构建领券记录CouponRecordDO couponRecordDO = new CouponRecordDO();BeanUtils.copyProperties(couponDO,couponRecordDO);couponRecordDO.setCreateTime(new Date());couponRecordDO.setUseState(CouponStateEnum.NEW.name());couponRecordDO.setUserId(loginUser.getId());couponRecordDO.setUserName(loginUser.getName());couponRecordDO.setCouponId(couponDO.getId());couponRecordDO.setId(null);int row = couponMapper.reduceStock(couponId);if(row == 1){couponRecordMapper.insert(couponRecordDO);}else{log.info("发送优惠券失败:{},用户:{}",couponDO,loginUser);}
}

这样,经过Jmeter的压测优惠券并没有出现超发的情况。

虽然这样可以解决超发的问题,但是在项目中我们不可以这样写,原因如下:

  • synchronized的作用范围是单个JVM实例,如果是集群部署系统这里的加锁你可以理解成失效
  • 在使用了synchronized加锁后,就会形成串行等待的问题,当一个线程A在领取优惠券方法内执行过久时,其它线程会等待直到线程A执行结束

解决方案二(Sql层面解决超发)

<update id="reduceStock">update coupon set stock = stock - 1 where id = #{coupon_id} and stock > 0
</update>

Mysql默认使用的是InnoDB引擎,使用InnoDB时在修改某一个记录的时候会将这条记录上锁,所以这个修改数据时不会出现多个线程同时修改数据。这样也可以避免优惠券超发。

如果在业务中只要有库存就可以发放优惠券的可以使用上面这种方式。

还有一种Sql的方式,可以将stock自身做为乐观锁。

<update id="reduceStock">update product set stock=stock-1 where stock=#{上一次的库存}  and id = 1 and stock>0
</update>

上面这种方式会存在ABA的问题,当然如果业务不在意ABA问题可以使用上面的sql,不过性能可能差一点,如果stock不匹配,这条sql也就失效了。

如果业务在意ABA问题的话也可以在表中加一个version的字段,每次修改数据的时候这个字段会加1,这样就可以避免ABA问题

<update id="reduceStock">update product set stock=stock-1,versioin = version+1 where  id = 1 and stock>0 and version=#{上一次的版本号}
</update>

上面的这三条Sql层面的代码都可以解决优惠券超发的问题,具体使用那种就根据业务来选择了

解决方案三(通过Redis分布式锁来解决问题)

引入Redis后,当领取优惠券时会先去Redis里面去获取锁,当锁获取成功后才可以对数据库进行操作

在分布式锁中我们应该考滤如下:

  • 排他性,在分布式集群中,同一个方法,在同一个时间只能被某一台机器上的一个线程执行
  • 容错性,当一个线程上锁后,如果机器突然的宕机,如果不释放锁,此时这条数据将会被锁死
  • 还要注意锁的粒度,锁的开销
  • 满足高可用,高性能,可重入

我们可以使用Redis里面的setnx命令来设置锁,因为setnx是原子性的操作不可被打断

当这个命令执行成功的时候会返回1,执行失败会返回0,我们就可以通过这个特性来判断是否获取到了锁。

先看下伪代码:

String key = "lock:coupon:" + couponId;
try{if(setnx(key,"1")){//获取到锁//设置Key的时期时间exp(key,30,TimeUnit.MILLISECONDS);try{//业务逻辑}finally{del(key);}}else{//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁}
}

这方法里面设置key的过期时间的原因是,当机器突然的宕机后,即使没有释放掉锁,他也会在一段时间后将这个锁释放,避免导致死锁。

虽然看上面的代码是没有问题的,但是它是存在一个误删除key的问题

为了避免这个问题,可以将setnx命令设置的那个值,设置成当前线程的ID,在删除的时候判断这个线程ID是不是与当前线程的Id相同就可以了。

String key = "lock:coupon:" + couponId;
String threadId = Thread.currentThread().getId();
try{if(setnx(key,threadId)){//获取到锁//设置Key的时期时间exp(key,30,TimeUnit.MILLISECONDS);try{//业务逻辑}finally{if(get(key) == threadId){del(key);}}}else{//获取锁失败,递归调用这个方法,或者使用for进行自旋获取锁}
}

通过上面这种方法就可以解决误删除key的问题。

在finally中的这个判断和删除key的代码不是原子性的,我们可以通过lua脚本的方式来实现它们之间的原子性,将删除key的代码修改成如下:

String script = "if redis.call('get',KEYS[1]) == ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";redisTemplate.execute(new DefaultRedisScript<>(script, Integer.class), Arrays.asList(key), threadId);

这里的threadId其实也可以不用,写成uuid也可以,但是在上面setnx的时候,那个值也要写成uuid

但是这样还要存在一个锁自动续期的问题,你可以开一个守护线程,每隔多久给他续期一次,或者是直接将这个过期时间延长一些。

在Redis中也有一些官方推荐的分布式锁的方式。我最后是使用的这种方式

解决方案四(使用Redis推荐的方式)

官网地址:redis.io/docs/refere…

这个有多种实现方式,比如:Golang,Java,Php

引入Redisson包

<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.17.4</version>
</dependency>

配置RedissoneClient

@Configuration
public class AppConfig {@Value("${spring.redis.host}")private String redisHost;@Value("${spring.redis.port}")private String redisPort;@Beanpublic RedissonClient redisson(){Config config = new Config();config.useSingleServer().setAddress("redis://" + redisHost + ":" + redisPort);return Redisson.create(config);}
}

配置好RedissonClient后,通过getLock方法获取到锁对象后,在我们的Service层中就可以通过lock和unlock来进行加锁和释放锁了,这样还是很方便的。

public JsonData addCoupon(long couponId, CouponCategoryEnum categoryEnum) {String key = "lock:coupon:" + couponId;RLock rLock = redisson.getLock(key);LoginUser loginUser = LoginInterceptor.threadLocal.get();rLock.lock();try{//业务逻辑}finally {rLock.unlock();}return JsonData.buildSuccess();
}

通过这种方法也可以解决优惠券超发的问题 ,这也是Rediss官网推荐的一种方式。

使用这种方式也无需关心key过期时间续期的问题,因为在Redisson一旦加锁成功,就会启动一个watch dog,你可以将它理解成一个守护线程,它默认会每隔30秒检查一下,如果当前客户端还占有这把锁,它会自动对这个锁的过期时间进行延长。

也可以通过下面的方法设置watch dog的检测时间间隔

Config config = new Config();
config.setLockWatchdogTimeout();

在高并发情况下我是这样解决单用户超领优惠券问题相关推荐

  1. 高并发情况下解决单用户超领优惠券问题

    问题抛出 在近期的项目里面有一个功能是领取优惠券的功能, 问题描述: 每一个优惠券一共发行多少张,每个用户可以领取多少张: 如:A优惠券一共发行120张,每一个用户可以领取140张,当一个用户领取优惠 ...

  2. 在高并发情况下如何解决用户超领优惠券问题

    在高并发情况下如何解决单用户超领优惠券问题 一. 场景描述 在近期的项目里面有一个功能是领取优惠券的功能,每一个优惠券一共发行多少张,每个用户可以领取多少张: 如:A优惠券一共发行120张,每一个用户 ...

  3. 高并发情况下修改系统参数

    单进程最大打开文件数限制 一般的发行版,限制单进程最大可以打开1024个文件,这是远远不能满足高并发需求的,调整过程如下: 在#号提示符下敲入: ulimit–n 65535 限制修改失败了,会显示 ...

  4. wcf高并发 mysql_WCF服务在高并发情况下报目标积极拒绝的异常处理 z

    http://www.cnblogs.com/kklldog/p/5037006.html wcf的监控服务,偶尔监控到目标服务会报一个目标积极拒绝的错误.一开始以为服务停止了,上服务器检查目标服务好 ...

  5. java rabbitmq 并发_RabbitMQ消息中间件 高级篇二 高并发情况下保障消息投递可靠性...

    RabbitMQ消息中间件技术精讲9 高级篇二 高并发场景下,消息的延迟投递做二次确认进行回调检查来保障生产者消息投递成功的可靠性 在上一篇文章中,我们介绍了BAT大厂中一种方式保障生成者消息投递可靠 ...

  6. 随手记录第二话 -- 高并发情况下秒杀、抢红包都有哪些实现方式?

    1.何为高并发? 高并发:在短时间内涌入超量的请求 那么如果出现这几种情况,可能会导致的后果 服务宕机 商品库存,红包金额超量 2.何为高并发秒杀? 这是一个高频面试题,问题虽然简单,但是里面的细节有 ...

  7. 缩短服务器响应时间,美国服务器高并发情况下缩短响应时间的方法

    美国服务器网站的响应时间是指系统响应请求的时间.网站响应时间越短,用户访问美国服务器网站的速度就越快.虽然响应时间并不直接反映网站的性能,但它在一定程度上反映了美国服务器网站系统的处理能力.下面将讨论 ...

  8. 集群高并发情况下如何保证分布式唯一全局ID生成

    作者:轻狂书生 blog.csdn.net/LookForDream_/article/details/109355335 前言 系统唯一ID是我们在设计一个系统的时候常常会遇见的问题,也常常为这个问 ...

  9. MySql(15)——Mysql在高并发情况下,防止库存超卖而小于0的解决方案

    本人上次做申领campaign的PHP后台时,因为项目上线后某些时段同时申领的人过多,导致一些专柜的存货为负数(<0),还好并发量不是特别大,只存在于小部分专柜而且一般都是-1的状况,没有造成特 ...

最新文章

  1. 从微盟36小时故障,谈谈数据安全这点事
  2. FirstDay@JavaOne2017
  3. Bootstrap4 glyphicon 移除图标 glyphicon fonts-faces 解决方案
  4. 对网络中安全审计产品的理解
  5. php 多只能上传20个文件解决办法,修改php.ini 的max_file_uploads
  6. 转载ASP.NET MVC 中@Html.Partial,@Html.Action,@Html.RenderPartial,@Html.RenderAction区别
  7. 【设计模式】依赖倒转原则
  8. 一文搞定 Hive 表分隔符
  9. 软件架构设计_软件架构设计的三个维度,软件架构师需要知道的点,了解一下吧...
  10. 我只是bug的搬运工之idea的Cannot run program git.exe: CreateProcess error=2
  11. nyoj244 16进制的简单运算
  12. 【学习笔记】用Excel制作随机抽奖器(Visual Basic编程)。
  13. scratch小游戏2048
  14. 现有数据整合方案介绍
  15. 51单片机八段数码c语言程序,51单片机做的音乐盒,带八段数码管显示程序+Proteus仿真...
  16. 在线photoshop工具网站
  17. HTML5常用的文本标签及css字体样式属性
  18. XP与Win7默认下时间分隔符不同
  19. Sasha and a Bit of Relax K倍区间 (前缀和异或 前缀和计数 思维)
  20. 解决WPS字体的问题

热门文章

  1. HDU1181:变形课(并查集 + DFS + BFS)
  2. 进程未知端口占用通用解决方法,Socket Error 10013解决办法,实测有效
  3. 【Linux基础】Linux下查看硬件配置
  4. 一篇文章带你搞定线程池的自定义创建和扩展
  5. Coremail保障企业邮箱安全!《数安法》《关基保护条例》正式施行!
  6. 开源一个golang小程序商城后台系统(moshopserver)
  7. Check Box的详细用法
  8. 蜘蛛爬行策略或网络抓取
  9. 问题 C: 总绩点计算
  10. 多种退出vim的方法