高并发问题——抢红包案例分析(一)

学习了大佬的博客,跑了下demo,但是失败了。

MVC那里也不知道为啥拦截不到我的请求,大佬原文地址

https://blog.csdn.net/yangshangwei/article/details/82975845

1.问题描述:

模拟 20 万元的红包,共分为 2 万个可抢的小红包,有 3 万人同时抢夺的场景 ,模拟出现超发和如何保证数据一致性的问题。

案例关注点:

数据一致性系统的性能

在这里插入图片描述

使用 SQL 去查询红包的库存、发放红包的总个数、总金额,我们发现了错误,红包总额为 20 万元,两万个小红包,结果发放了 200020元的红包, 20002 个红包。现有库存为-2,超出了之前的限定,这就是高并发的超发现象,这是一个错误的逻辑 。

2.问题分析

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

2.1使用数据库锁的解决方案

2.1.1 悲观锁

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,依次类推

一致性数据统计:

在这里插入图片描述

性能数据统计:

在这里插入图片描述


2.1.2 乐观锁

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

这样,保证了修改的数据是和它查询出来的数据是一致的,而其他线程并未进行修改。当然,如果更新失败,表示在更新操作之前有其他线程已经更新了该红包数,那么就可以尝试重入机制来保证更新成功。
在扣减红包的时候 , 增加了对版本号的判断,其次每次扣减都会对版本号加一,这样保证每次更新在版本号上有记录 , 从而避免 ABA 问题

一致性数据统计:                                                             

在这里插入图片描述

性能数据统计:

在这里插入图片描述

解决因version导致失败问题

为提高成功率,可以考虑使用重入机制 。 也就是一旦因为版本原因没有抢到红包,则重新尝试抢红包,但是过多的重入会造成大量的 SQL 执行,所以目前流行的重入会加入两种限制

1.一种是按时间戳的重入,也就是在一定时间戳内(比如说 100毫秒),不成功的会循环到成功为止,直至超过时间戳,不成功才会退出,返回失败。
2.一种是按次数,比如限定 3 次,程序尝试超过 3 次抢红包后,就判定请求失效,这样有助于提高用户抢红包的成功率。

 

乐观锁重入机制-按时间戳重入

/**
	 * 
	 * 
	 * 乐观锁,按时间戳重入
	 * 
	 * @Description: 乐观锁,按时间戳重入
	 * 
	 * @param redPacketId
	 * @param userId
	 * @return
	 * 
	 * @return: int
	 */
	@Override
	@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
	public int grapRedPacketForVersion(Long redPacketId, Long userId) {
		// 记录开始时间
		long start = System.currentTimeMillis();
		// 无限循环,等待成功或者时间满100毫秒退出
		while (true) {
			// 获取循环当前时间
			long end = System.currentTimeMillis();
			// 当前时间已经超过100毫秒,返回失败
			if (end - start > 100) {
				return FAILED;
			}
			// 获取红包信息,注意version值
			RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
			// 当前小红包库存大于0
			if (redPacket.getStock() > 0) {
				// 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
				int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
				// 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
				if (update == 0) {
					continue;
				}
				// 生成抢红包信息
				UserRedPacket userRedPacket = new UserRedPacket();
				userRedPacket.setRedPacketId(redPacketId);
				userRedPacket.setUserId(userId);
				userRedPacket.setAmount(redPacket.getUnitAmount());
				userRedPacket.setNote("抢红包 " + redPacketId);
				// 插入抢红包信息
				int result = userRedPacketDao.grapRedPacket(userRedPacket);
				return result;
			} else {
				// 一旦没有库存,则马上返回
				return FAILED;
			}
		}
	}



当因为版本号原因更新失败后,会重新尝试抢夺红包,但是会实现判断时间戳,如果时间戳在 100 毫秒内,就继续,否则就不再重新尝试,而判定失败,这样可以避免过多的SQL 执行 , 维持系统稳定。

一致性测试:

在这里插入图片描述

性能测试:

在这里插入图片描述

乐观锁重入机制-按次数重入

/**
	 * 
	 * 
	 * @Title: grapRedPacketForVersion
	 * 
	 * @Description: 乐观锁,按次数重入
	 * 
	 * @param redPacketId
	 * @param userId
	 * 
	 * @return: int
	 */
	@Override
	@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRED)
	public int grapRedPacketForVersion(Long redPacketId, Long userId) {
		for (int i = 0; i < 3; i++) {
			// 获取红包信息,注意version值
			RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
			// 当前小红包库存大于0
			if (redPacket.getStock() > 0) {
				// 再次传入线程保存的version旧值给SQL判断,是否有其他线程修改过数据
				int update = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
				// 如果没有数据更新,则说明其他线程已经修改过数据,则重新抢夺
				if (update == 0) {
					continue;
				}
				// 生成抢红包信息
				UserRedPacket userRedPacket = new UserRedPacket();
				userRedPacket.setRedPacketId(redPacketId);
				userRedPacket.setUserId(userId);
				userRedPacket.setAmount(redPacket.getUnitAmount());
				userRedPacket.setNote("抢红包 " + redPacketId);
				// 插入抢红包信息
				int result = userRedPacketDao.grapRedPacket(userRedPacket);
				return result;
			} else {
				// 一旦没有库存,则马上返回
				return FAILED;
			}
		}
		return FAILED;
	}


 

通过 for 循环限定重试 3 次, 3 次过后无论成败都会判定为失败而退出 , 这样就能避免过多的重试导致过多 SQL 被执行的问题,从而保证数据库的性能.

数据一致性和性能截图

在这里插入图片描述

在这里插入图片描述

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值