SSM+Redis高并发抢红包之-乐观锁

继上次的SSM+Redis高并发抢红包之-悲观锁之后,现在我来写下如何使用乐观锁了解决抢红包的并发问题

 

首先我们先了解下什么是乐观锁(也叫非阻塞锁)

乐观锁是一种不会阻塞其他线程并发的机制,它不会使用数据库的锁进行实现,由于它不阻塞其他线程,所以不会引发线程频繁的挂起和恢复,这样就能够提高并发能力。乐观锁使用的是CAS原理

关于什么是CAS原理,这里简单讲下: 在一个多线程的环境中,有个共同的资源,那么首先我们先保存一个旧值,然后经过一定的逻辑处理,当需要进行某个操作的时候(这里可以理解为当扣减红包的时候),先比较数据库当前的值和旧值是否一致,如果一致就进行红包扣减,否则就不执行这个操作,大概内容就是这样,详细的可以看看这篇博客CAS原理及所导致的ABA问题,关于其底层实现可以看看这篇:CAS底层实现

由于CAS原理的使用会产生一个问题,那就是ABA问题,关于啥是ABA,上面那个链接博客,可以去看下。

那我们如何解决这个问题呢,一般都设一个专属字段,大多数情况下都设一个version 版本号的字段,这个字段是只能自增的,通过控制version的值,然后每次更新数据是进行比较version值,就可以解决ABA问题了

继上次的代码,这里只需在RedPacketDao 接口中添加

/*
	 * 扣减抢红包数 --乐观锁使用版本号来控制
	 * @param id 红包id
	 * @param version 版本号
	 * @return 更新记录条数
	 */
	public int decreaseRedPacketForVersion(@Param("id") Long id, @Param("version") Integer version);

其次在 RedPacket中添加一条sql语句

<!-- 扣减抢红包库存 ,没更新一次,版本加1,其次增加对版本号的判断-->
	<update id="decreaseRedPacketForVersion">
		update T_RED_PACKET set stock = stock - 1,version = version + 1
		 where id = #{id} and version = #{version}
	</update>

为了方便测试,我们在UserRedPacketService中新增一个接口方法

	/*
	 * 保存抢红包信息-保存version
	 * @param redPacketId 红包编号
	 * @param userId      抢红包用户编号
	 * @return            影响记录数
	 */
	public int grapRedPacketForVersion(Long redPacketId, Long userId);

当然,也需要在impl中进行实现

    @Override
	@Transactional(isolation=Isolation.READ_COMMITTED, propagation=Propagation.REQUIRED)
	public int grapRedPacketForVersion(Long redPacketId, Long userId) {
		// TODO Auto-generated method stub
		
		//获取红包信息,这里注意下version值
		RedPacket redPacket = redPacketDao.getRedPacket(redPacketId);
		//当小红包库存大于0
		if(redPacket.getStock() > 0) {
			//再次传入线程保存的version 旧值给SQL判断,是否有其他线程修改过数据
			int updateFlag = redPacketDao.decreaseRedPacketForVersion(redPacketId, redPacket.getVersion());
			//如果没有数据更新,则说明其他线程已经修改过数据,本次抢红包失败
			if(updateFlag == 0) {
				return FAILED;
			}
			//生成抢红包信息
			UserRedPacket userRedPacket = new UserRedPacket();
			userRedPacket.setRedPacketId(redPacketId);
			userRedPacket.setRedPacketId(redPacketId);
			userRedPacket.setUserId(userId);
			userRedPacket.setAmount(redPacket.getUnitAmount());
			userRedPacket.setNote("抢红包 "+ redPacketId);
			//插入抢红包信息
			int result = userRedPacketDao.grapRedPacket(userRedPacket);
			return result;
			
		}
		return FAILED;
	}

 

最后在UserRedPacketController中新开一个方法

@RequestMapping("/grapRedPacketForVersion")
	@ResponseBody
	public Map<String, Object> grapRedPacketForVersion(Long redPacketId, Long userId){
		
		//抢红包
		int result = userRedPacketService.grapRedPacketForVersion(redPacketId, userId);
		Map<String,Object> res = new HashMap<String,Object>();
		boolean flag = result > 0;
		
		res.put("success", flag);
		res.put("message", flag?"抢红包成功":"抢红包失败");
		return res;
	}

然后就可以进行测试了

我们继续在t_red_packet中插入200000元, 20000个红包

stock还剩7297个,也就是存在大量因为版本不一致的原因造成抢红包失败,主要是因为版本的并发而产生的。

一般为了提高成功率,会考虑采取重入机制,重入机制有两种,一种是按时间戳重入,一种是按次数重入

1.按时间戳重入

按时间戳重入,只需在业务层UserRedPacketServiceImpl里写一个循环语句即可,这里我们设定时间戳为100ms,超过这个时间就退出循环

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

 

2、按次数也就是将while 改成for  限定i大小,就行了。

 

这两种都可以消除大量的请求失败,避免非重入的时候大量请求失败的场景。但是当数据量不断增大,直接使用数据库将变得很慢,这时候就可以选择性能更好的,通过Redis+Lua语言来处理高并发。关于Redis+Lua语言实现抢红包,可以看下我下篇博客SSM+Redis高并发抢红包之-Lua+Redis

详细代码可以在我GitHub上进行查看   ssm-redis

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值