嗯嗯,好长时间不更新博客了,感觉整个人都颓废了哈哈,不过呢我还是蛮怀恋这种指尖在键盘上敲打的感觉的。最近闲来无事,就在码云上看看有没有什么比较小的开源项目来学习学习(内容太多看不下去= =看了半天看到这么一个项目)
瞬间被吸引了,感觉挺牛般的,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
感觉作者还是蛮用心的,注释写的很清楚,也留了一些坑让我们自己去思考,总之呢是一个蛮适合学习的项目呢。
哎,我看了一下上次写博客的时间相差快一年了,感觉自己越来越颓废了,再这样下去真的要被淘汰了。
可能随着时间的推移,在我们迈入社会的时候,来自各方面的压力,工作、家庭让我们不堪重负,渐渐的成为了金钱的奴隶,拿着固定的工资,整天混吃等死,慢慢的遗忘了最初的梦想,也丢失了曾经的那颗初心。
但是某一天你回头看你走过的那些路,回想一下你曾经说过的过,你的心也会为之颤动。
在这里我还是摘录电影无问西东里面的一句话送给大家
愿你在迷茫时,坚信你的珍贵,爱你所爱,行你所行,听从你心,无问西东。