从0到1构建分布式秒杀系统,脱离案例讲架构都是耍流氓

嗯嗯,好长时间不更新博客了,感觉整个人都颓废了哈哈,不过呢我还是蛮怀恋这种指尖在键盘上敲打的感觉的。最近闲来无事,就在码云上看看有没有什么比较小的开源项目来学习学习(内容太多看不下去= =看了半天看到这么一个项目)

瞬间被吸引了,感觉挺牛般的,fork,star,download 三连击,下载,部署环境搞了老半天终于搞好了,然后运行测试美滋滋。

以上都是废话= =,言归正传。我今天主要就聊一下他的第二个接口的几点思考。

controller代码:

@ApiOperation(value="秒杀二(程序锁)",nickname="科帮网")
	@PostMapping("/startLock")
	public Result startLock(long seckillId){
		int skillNum = 1000;
		final CountDownLatch latch = new CountDownLatch(skillNum);//N个购买者
		seckillService.deleteSeckill(seckillId);
		final long killId =  seckillId;
		LOGGER.info("开始秒杀二(正常)");
		for(int i=0;i<1000;i++){
			final long userId = i;
			Runnable task = () -> {
				Result result = seckillService.startSeckilLock(killId, userId);
				LOGGER.info("用户:{}{}",userId,result.get("msg"));
				latch.countDown();
			};
			executor.execute(task);
		}
		try {
			latch.await();// 等待所有人任务结束
			Long  seckillCount = seckillService.getSeckillCount(seckillId);
			LOGGER.info("一共秒杀出{}件商品",seckillCount);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return Result.ok();
	}

service代码:

@Override
	@Transactional
	public Result  startSeckilLock(long seckillId, long userId) {
		 try {
			lock.lock();
			/**
			 * 1)这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象
			 * 2)来自热心网友 zoain 的细心测试思考、然后自己总结了一下,事物未提交之前,锁已经释放(事物提交是在整个方法执行完),导致另一个事物读取到了这个事物未提交的数据,也就是传说中的脏读。建议锁上移
			 * 3)给自己留个坑思考:为什么分布式锁(zk和redis)没有问题?(事实是有问题的,由于redis释放锁需要远程通信,不那么明显而已)
			 * 4)2018年12月35日,更正一下,之前的解释(脏读)可能给大家一些误导,数据库默认的事务隔离级别为 可重复读(repeatable-read),也就不可能出现脏读
			 * 哪个这个级别是只能是幻读了?分析一下:幻读侧重于新增或删除,这里显然不是,那这里到底是什么,给各位大婶留个坑~~~~
			 */
			String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
			Object object =  dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
			Long number =  ((Number) object).longValue();
			if(number>0){
				nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
				dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
				SuccessKilled killed = new SuccessKilled();
				killed.setSeckillId(seckillId);
				killed.setUserId(userId);
				killed.setState(Short.parseShort(number+""));
				killed.setCreateTime(new Timestamp(new Date().getTime()));
				dynamicQuery.save(killed);
			}else{
				return Result.error(SeckillStatEnum.END);
			}
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
		return Result.ok(SeckillStatEnum.SUCCESS);
	}

这里的锁用的是:

	private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁

然后执行接口的时候出现了超卖101的情况,也就是库存为负数。

刚开始我想是不是它这个锁用的不对,这个lock锁是不是有什么问题,或者是它加锁的方式不对?然后我查阅了相关资料

发现了一句话加锁的时候必须保证这个锁是同一个对象。我就用synchronized同步块改造了一下代码保证锁是一个锁,也就是传入同一个对象

@Override
	@Transactional
	public Result  startSeckilLock(long seckillId, long userId,Object obj) {
		 try {
			//lock.lock();
			 synchronized (obj) {
				 /**
				  * 1)这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象
				  * 2)来自热心网友 zoain 的细心测试思考、然后自己总结了一下,事物未提交之前,锁已经释放(事物提交是在整个方法执行完),导致另一个事物读取到了这个事物未提交的数据,也就是传说中的脏读。建议锁上移
				  * 3)给自己留个坑思考:为什么分布式锁(zk和redis)没有问题?(事实是有问题的,由于redis释放锁需要远程通信,不那么明显而已)
				  * 4)2018年12月35日,更正一下,之前的解释(脏读)可能给大家一些误导,数据库默认的事务隔离级别为 可重复读(repeatable-read),也就不可能出现脏读
				  * 哪个这个级别是只能是幻读了?分析一下:幻读侧重于新增或删除,这里显然不是,那这里到底是什么,给各位大婶留个坑~~~~
				  */
				 String nativeSql = "SELECT number FROM seckill WHERE seckill_id=?";
				 Object object = dynamicQuery.nativeQueryObject(nativeSql, new Object[]{seckillId});
				 Long number = ((Number) object).longValue();
				 if (number > 0) {
					 nativeSql = "UPDATE seckill  SET number=number-1 WHERE seckill_id=?";
					 dynamicQuery.nativeExecuteUpdate(nativeSql, new Object[]{seckillId});
					 SuccessKilled killed = new SuccessKilled();
					 killed.setSeckillId(seckillId);
					 killed.setUserId(userId);
					 killed.setState(Short.parseShort(number + ""));
					 killed.setCreateTime(new Timestamp(new Date().getTime()));
					 dynamicQuery.save(killed);
				 } else {
					 return Result.error(SeckillStatEnum.END);
				 }
			 }
		} catch (Exception e) {
			e.printStackTrace();
		}finally {
			lock.unlock();
		}
		return Result.ok(SeckillStatEnum.SUCCESS);
	}

结果失败 = =,还是出现超卖101的情况。

然后我就想这个代码层面的锁可能并没有问题,会不会是数据库层面的锁的问题。

查阅相关资料

https://blog.csdn.net/YXX_decsdn/article/details/91347114

顿悟原来根本原因是因为mysql的可重复读的原因,处理方式也是欠妥的,这个lock锁是加在了事务的里面,也就是说在事务没有提交的时候这个锁就释放了,而另一个线程因为mysql可重复读的原因导致读的数据还是前一秒事务未提交的数据,在那边事务提交的一瞬间,我们这边的库存变量number就发生了变化,所以就超卖了。

详细说明可参考作者博文

https://blog.52itstyle.vip/archives/2952/

嗯嗯原因是知道了,那怎么解决呢

我看了一下网站的评论大致有三种解决方案

1.修改事务的隔离级别。

2.修改锁的方式,将锁放在事务的外面包起来也就是作者的解决方式aop+锁方式。

3.sql语句修改加条件。

至于怎么改嘛就仁者见仁,智者见智呢。我也不想继续深究了。

看了一下作者说这里为什么不用synchronized的方式呢。查阅相关资料 = =只会百度的zz= =

https://blog.csdn.net/zxd8080666/article/details/83214089

看了这篇文章我发现了值得注意的两点

1.synchronized是非公平锁,ReentrantLock默认也是非公平锁,但是它是可设置的。

2.synchronized会出现死锁现象,但是ReenTrantLock不会。

查阅相关资料

https://blog.csdn.net/zhaoziyun21/article/details/89462798

哇哦,原来所谓公平锁和非公平锁的区别就是先来后到,要排队滴啊。

那么再联系我们这个应用场景,结果不言而喻。

有兴趣的小伙伴可以看看这个项目

https://gitee.com/sub_callow/spring-boot-seckill

感觉作者还是蛮用心的,注释写的很清楚,也留了一些坑让我们自己去思考,总之呢是一个蛮适合学习的项目呢。

哎,我看了一下上次写博客的时间相差快一年了,感觉自己越来越颓废了,再这样下去真的要被淘汰了。

可能随着时间的推移,在我们迈入社会的时候,来自各方面的压力,工作、家庭让我们不堪重负,渐渐的成为了金钱的奴隶,拿着固定的工资,整天混吃等死,慢慢的遗忘了最初的梦想,也丢失了曾经的那颗初心。

但是某一天你回头看你走过的那些路,回想一下你曾经说过的过,你的心也会为之颤动。

在这里我还是摘录电影无问西东里面的一句话送给大家

愿你在迷茫时,坚信你的珍贵,爱你所爱,行你所行,听从你心,无问西东。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值