实战高并发秒杀实现(2):防止库存超卖问题(超详细)

一、理论基础

1.1、防止库存超卖,需要关注的问题

(1)秒杀数据库的设计;

(2)基于数据库乐观锁防止库存超卖;

(3)基于redis实现用户行为频率限制——用户再次抢购时提示“该用户操作频繁,请少稍后重试,一般可设置10秒后才能再次调用秒杀接口”;

(4)基于Token令牌+MQ实现异步修改库存;

(5)使用apache-jmeter做秒杀压力测试(可配置线程数和循环次数(每个线程跑多少个请求数),比如线程数100循环次数100,则模式10000次请求)。

1.2、数据库崩溃问题

问题:

如果秒杀的请求过多,对数据库频繁的IO操作,可能会产生数据库崩溃问题。这时搞分表分库、读写分离、做缓存、限流、熔断都不会起作用。——最有用的是,提前生成令牌,存放在临牌桶中,异步发送到MQ中(token只经过缓存不经过数据库),MQ异步修改库存

解决方案:

假设库存有100个,但是可能会有10万个并发,要解决数据库频繁IO,可以提前生成好数据库 库存个数个Token,比如这里是100个Token,比如这时有10万个并发,谁能抢到Token,再把Token扔到MQ里面,在MQ里面异步实现修改库存。这时就能做到多少个库存有多少个请求到数据库,而不是10万个请求访问10万次数据库,防止了没抢到的用户无法修改数据库,从而减少了IO操作

1.3、秒杀骨架图

1.4、总体实现步骤

(1)后台系统在发布秒杀商品的时候,给对应商品添加库存token;

二、实战

2.1、秒杀数据库设计

(1)秒杀 成功明细(记录)表

秒杀抢购的订单和普通下单的订单是完全不一样的

CREATE TABLE `shop_order` (
  `seckill_id` bigint(20) NOT NULL COMMENT '秒杀商品id',
  `user_phone` bigint(20) NOT NULL COMMENT '用户手机号',
  `state` tinyint(4) NOT NULL DEFAULT '-1' COMMENT '状态标示:-1:无效 0:成功 1:已付款 2:已发货',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';

(2)秒杀 库存表

比如jmeter模拟时发出10000个请求,但是库存只有100,则有9900个用户抢不到。

version字段:代表更新次数,默认是0,库存没更新一次则加1,比如库存1000,变为997,则version值变为3,并且成功明细表有3条数据。

CREATE TABLE `shop_seckill` (
  `seckill_id` bigint(20) NOT NULL COMMENT '商品库存id',
  `name` varchar(120) CHARACTER SET utf8 NOT NULL COMMENT '商品名称',
  `inventory` int(11) NOT NULL COMMENT '库存数量',
  `start_time` datetime NOT NULL COMMENT '秒杀开启时间',
  `end_time` datetime NOT NULL COMMENT '秒杀结束时间',
  `create_time` datetime NOT NULL COMMENT '创建时间',
  `version` bigint(20) NOT NULL DEFAULT '0',
  PRIMARY KEY (`seckill_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='秒杀库存表';

2.2、后台系统在发布秒杀商品的时候,给对应商品添加库存token

新增对应商品令牌桶:

/**
	 * 新增对应商品库存令牌桶
	 * @seckillId 商品库存id
	 */
	@RequestMapping("/addSpikeToken")
	public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity);

@Async
	private void createSeckillToken(Long seckillId, Long tokenQuantity) {
		generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
	}

2.2、秒杀商品服务接口

(1)SpikeCommodityService:

点击立即抢购(秒杀)按钮,接口传入电话或者用户ID+库存商品ID (秒杀 库存表的库存ID

/**
 * 秒杀商品服务接口
 */
public interface SpikeCommodityService {

	/**
	 * 用户秒杀接口 phone和userid都可以的
	 * 
	 * @phone 手机号码<br>
	 * @seckillId 库存id
	 * @return
	 */
	@RequestMapping("/spike")
	public BaseResponse<JSONObject> spike(String phone, Long seckillId);

	/**
	 * 新增对应商品库存令牌桶
	 * 
	 * @seckillId 商品库存id
	 */
	@RequestMapping("/addSpikeToken")
	public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity);

}

(2)SpikeCommodityServiceImpl:

秒杀接口实现步骤:

  • 参数验证;
  • 从redis从获取对应的秒杀token——采用redis数据库类型为 list类型, key为 商品库存id ,list存 多个秒杀token,一个库存ID可以装多个秒杀token;
  •  获取到秒杀token之后,异步(@Async异步注解)放入mq中实现修改商品的库存——多线程异步生成token
  • 方法添加@HystrixCommand(fallbackMethod = "spikeFallback")——实现服务隔离和降级
@RestController
@Slf4j
public class SpikeCommodityServiceImpl extends BaseApiService<JSONObject> implements SpikeCommodityService {
	@Autowired
	private SeckillMapper seckillMapper;
	@Autowired
	private GenerateToken generateToken;
	@Autowired
	private SpikeCommodityProducer spikeCommodityProducer;

	@Override
	@Transactional
    @HystrixCommand(fallbackMethod = "spikeFallback")
	public BaseResponse<JSONObject> spike(String phone, Long seckillId) {
		// 1.参数验证
		if (StringUtils.isEmpty(phone)) {
			return setResultError("手机号码不能为空!");
		}
		if (seckillId == null) {
			return setResultError("商品库存id不能为空!");
		}
		// 2.从redis从获取对应的秒杀token
		String seckillToken = generateToken.getListKeyToken(seckillId + "");
		if (StringUtils.isEmpty(seckillToken)) {
			log.info(">>>seckillId:{}, 亲,该秒杀已经售空,请下次再来!", seckillId);
			return setResultError("亲,该秒杀已经售空,请下次再来!");
		}

		// 3.获取到秒杀token之后,异步放入mq中实现修改商品的库存
		sendSeckillMsg(seckillId, phone);
		return setResultSuccess("正在排队中.......");
	}

	/**
	 * 获取到秒杀token之后,异步放入mq中实现修改商品的库存
	 */
	@Async
	private void sendSeckillMsg(Long seckillId, String phone) {
		JSONObject jsonObject = new JSONObject();
		jsonObject.put("seckillId", seckillId);
		jsonObject.put("phone", phone);
		spikeCommodityProducer.send(jsonObject);
	}

	/**
	 * 使用多线程异步生产令牌
	 * 
	 * @param seckillId
	 * @param tokenQuantity
	 * @return
	 */

	// 采用redis数据库类型为 list类型 key为 商品库存id list 多个秒杀token
	@Override
	public BaseResponse<JSONObject> addSpikeToken(Long seckillId, Long tokenQuantity) {
		// 1.验证参数
		if (seckillId == null) {
			return setResultError("商品库存id不能为空!");
		}
		if (tokenQuantity == null) {
			return setResultError("token数量不能为空!");
		}
		SeckillEntity seckillEntity = seckillMapper.findBySeckillId(seckillId);
		if (seckillEntity == null) {
			return setResultError("商品信息不存在!");
		}
		// 2.使用多线程异步生产令牌
		createSeckillToken(seckillId, tokenQuantity);
		return setResultSuccess("令牌正在生成中.....");
	}

	@Async
	private void createSeckillToken(Long seckillId, Long tokenQuantity) {
		generateToken.createListToken("seckill_", seckillId + "", tokenQuantity);
	}

}

(3)修改库存:SeckillMapper

方案1:行锁机制(悲观锁)——数据库自带

如果不适用乐观锁防止超卖,直接更新数据库时使用一个“and inventory>0方式”——库存大于零才更新就可以了。

然后,mysql中每次在更新数据库时有行锁机制(悲观锁),不存在超卖问题

update shop_seckill set inventory=inventory-1,where  seckill_id=#{seckillId} and inventory>0 

方案2:version乐观锁实现乐观锁是通过version版本号控制的

version版本号乐观锁机制:

多个线程同时update的时候只有一个能成功,谁成功谁拿到锁。成功的线程修改成功时版本号加1,所以其余的线程就无法更新了(因为版本号变了)

update shop_seckill set inventory=inventory-1, version=version+1 where  seckill_id=#{seckillId} and inventory>0  and version=#{version} ;

以下是悲观锁的实现: 

public interface SeckillMapper {

	/**
	 * 使用乐观锁修改库存信息 and inventory>0方式
	 * @param seckillId
	 * @return
	 */
	@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001' and inventory>0")
	int optimisticLockSeckill(Long seckillId);

	/**
	 * 基于版本号形式实现乐观锁
	 * @param seckillId
	 * @return
	 */
	@Update("update meite_seckill set inventory=inventory-1 ,version=version+1 where  seckill_id=#{seckillId} and version=#{version} and inventory>0;")
	int optimisticVersionSeckill(@Param("seckillId") Long seckillId, @Param("version") Long version);

	@Update("update meite_seckill set inventory=inventory-1 where  seckill_id='10001';")
	int inventoryDeduction(Long seckillId);

	@Select("SELECT seckill_id AS seckillId,name as name,inventory as inventory,start_time as startTime,end_time as endTime,create_time as createTime,version as version from meite_seckill where seckill_id=#{seckillId}")
	SeckillEntity findBySeckillId(Long seckillId);

}

  上一篇:抢购理论研究

  下一篇:基于token+MQ实现修改库存

若对你有帮助,欢迎关注!!点赞!!评论!!

  • 24
    点赞
  • 169
    收藏
    觉得还不错? 一键收藏
  • 23
    评论
在golang中,高并发项目实战可以通过使用goroutines和channels来实现。其中,goroutines是轻量级的线程,可以并发执行多个任务,而channels则用于goroutines之间的通信。 一个常见的高并发项目实战是使用golang开发网络爬虫。网络爬虫是一种通过自动化程序浏览互联网,并从网页中提取数据的技术。在golang中,可以使用第三方的爬虫框架来简化开发过程,如引用所提到的爬虫框架。 在这个项目中,可以将爬虫框架的处理模块分为下载器、分析器和条目处理器。下载器负责从指定的URL下载网页内容,分析器用于解析网页并提取需要的数据,而条目处理器则对提取的数据进行处理。此外,还需要一个调度和协调这些处理模块运行的控制模块来确保它们按照预期的方式运行。 为了实现高并发,可以将每个任务(即要爬取的URL)分配给一个goroutine来执行。可以使用goroutine和channel的组合来实现任务的并发执行和结果的收集。引用中给出了一个示例函数CheckWebsites,该函数接收一个WebsiteChecker函数和一个URL列表,并返回一个映射,其中URL与检查结果对应。 在该示例函数中,使用了一个resultChannel来收集每个URL的检查结果。通过使用goroutine,在遍历URL列表时,可以将每个URL的检查结果发送到resultChannel中。然后,通过在循环中使用<-resultChannel来接收结果,并将结果存储在一个map中。 通过这种方式,可以实现高并发的URL检查,并在所有检查完成后返回结果。 总之,golang高并发项目实战可以通过使用goroutines和channels来实现并发执行任务和结果的收集。网络爬虫是一个常见的高并发项目实战示例,通过使用爬虫框架和适当的并发控制,可以实现高效的网页数据提取。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [golang GRPC 高并发实战1:基础环境搭建](https://blog.csdn.net/xatop/article/details/123326480)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *2* [golang爬虫框架,golang高并发实战](https://download.csdn.net/download/weixin_44772356/87273641)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] - *3* [golang高并发的思想与实践](https://blog.csdn.net/m0_58181788/article/details/123938547)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 33.333333333333336%"] [ .reference_list ]
评论 23
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值