全站资源开放下载,感谢广大网友的支持
链接失效请移步职涯宝平台的学习路线|资源下载分类
支持用户留言评论_客服实时在线_问题解决更快
支付宝赞助-Java帮帮社区
微信赞助-Java帮帮社区

从构建分布式秒杀系统聊聊Lock锁使用中的坑

9
发表时间:2018-11-08 13:16来源:Java帮帮-微信公众号


前言

在单体架构的秒杀活动中,为了减轻DB层的压力,这里我们采用了Lock锁来实现秒杀用户排队抢购。然而很不幸的是尽管使用了锁,但是测试过程中仍然会超卖,执行了N多次发现依然有问题。输出一下代码吧,可能大家看的比较真切:

@Service("seckillService")publicclassSeckillServiceImplimplementsISeckillService{    /**     * 思考:为什么不用synchronized   * service 默认是单例的,并发下lock只有一个实例     */private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁@Autowiredprivate DynamicQuery dynamicQuery;    @Override@Transactionalpublic Result  startSeckilLock(long seckillId, long userId){         try {            lock.lock();            //这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象            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);    }}

代码写在service层,bean默认是单例的,也就是说lock肯定是一个对象。感觉不放心,还是打印一下 lock.hashCode(),输出结果没问题。由于还有其他事情要做,最终还是带着疑问提交代码到码云。

追踪

如果想分享代码并使大家一起参与进来,一定要自荐,这样才会被更多的人发现。当然,如果有交流群一定要留下联系方式,这样讨论起来可能更方便。项目被推荐后,果然加群的小伙伴就多了。由于项目配置好相应参数就可以测试,并且每个点都有相应的文字注释,其中有心的小伙伴果然注意到了我写的注释<这里、不清楚为啥、总是会被超卖101、难道锁不起作用、lock是同一个对象>,然后提出了困扰自己好多天的问题。

码友zoain说,测试了好久终于发现了问题,原来lock锁是在事物单元中执行的。看到这里,小伙伴们有没有恍然大悟,反正我是悟了。这里,总结一下为什么会超卖101:秒杀开始后,某个事物在未提交之前,锁已经释放(事物提交是在整个方法执行完),导致下一个事物读取到了上个事物未提交的数据,也就是传说中的脏读。此处给出的建议是锁上移,也就是说要包住整个事物单元。

AOP+锁

为了包住事物单元,这里我们使用AOP切面编程,当然你也可以上移到Control层。

自定义注解Servicelock:

@Target({ElementType.PARAMETER, ElementType.METHOD})    Retention(RetentionPolicy.RUNTIME)    @Documentedpublic@interface Servicelock {      String description()default "";}

自定义切面LockAspect:

@Component@Scope@AspectpublicclassLockAspect{   /**      * 思考:为什么不用synchronized    * service 默认是单例的,并发下lock只有一个实例      */privatestatic  Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁  //Service层切点     用于记录错误日志@Pointcut("@annotation(com.itstyle.seckill.common.aop.Servicelock)")      publicvoidlockAspect(){            }        @Around("lockAspect()")    public  Object around(ProceedingJoinPoint joinPoint){         lock.lock();        Object obj = null;        try {            obj = joinPoint.proceed();        } catch (Throwable e) {            e.printStackTrace();        } finally{            lock.unlock();        }        return obj;    } }

切入秒杀方法:

@Service("seckillService")publicclassSeckillServiceImplimplementsISeckillService{    /**    * 思考:为什么不用synchronized    * service 默认是单例的,并发下lock只有一个实例    */private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁@Autowiredprivate DynamicQuery dynamicQuery;    @Override@Servicelock@Transactionalpublic Result startSeckilAopLock(long seckillId, long userId){        //来自码云码友<马丁的早晨>的建议 使用AOP + 锁实现        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);       }        return Result.ok(SeckillStatEnum.SUCCESS);   }}

所有的工作完成以后,我们来测试一下代码,意料之中,再也没有出现超卖的现象。然而,你以为就这么结束了么?细心的码友IM核米,又提出了以下问题:Spring 里的切片在未指定排序的时候,两个注解是随意执行的。如果事务在加锁前执行的话,是不是就会产生问题?

首先,由于自己实在没有时间去取证,最终还是码友IM核米完成了自问自答,这里引用下他的解释:

我说的没错,但 @Transactional 切片是特殊情况

1)多 AOP 之间的执行顺序在未指定时是 :undefined ,官方文档并没有说一定会按照注解的顺序进行执行,只会按照 @ Order 的顺序执行。

可参考官方文档: 可以在页面里搜索 Command+F「7.2.4.7 Advice ordering」https://docs.spring.io/spring/docs/3.0.x/spring-framework-reference/html/aop.html#aop-ataspectj-advice-ordering

2)事务切面的 default Order 被设置为了 Ordered.LOWEST_PRECEDENCE,所以默认情况下是属于最内层的环切。

可参考官方文档: 可以在页面里搜索 Command+F「Table 10.2. tx:annotation-driven/ settings」

https://docs.spring.io/spring/docs/3.0.x/reference/transaction.html#transaction-declarative-txadvice-settings

总结

经验真的很重要,踩的坑多了也变走成了路
不要吝啬自己的总结成果,分享交流才能够促使大家共同进步
最好不要怀疑久经考验的Lock锁同志,很有可能是你使用的方式不对



Java帮帮公众号生态

Java帮帮公众号生态

总有一款适合你

Java帮帮-微信公众号

Java帮帮-微信公众号

将分享做到极致

九点编程-公众号

九点编程-公众号

深夜九点学编程

大数据驿站-微信公众号

大数据驿站-微信公众号

一起在数据中成长

Python帮帮-公众号

Python帮帮-公众号

人工智能,爬虫,学习教程

程序员生活志-公众号

程序员生活志-公众号

互联网,职场,程序员那些事儿

Java帮帮学习群生态

Java帮帮学习群生态

总有一款能帮到你

Java学习群

Java学习群

与大牛一起交流

大数据学习群

大数据学习群

在数据中成长

九点编程学习群

九点编程学习群

深夜九点学编程

python学习群

python学习群

人工智能,爬虫

测试学习群

测试学习群

感受测试的魅力

Java帮帮生态承诺

Java帮帮生态承诺

一直坚守,不负重望

初心
勤俭
诚信
正义
分享
合作品牌 非盈利生态-优质内容分享传播者
友链交换:加帮主QQ2524138991 留言即可 24小时内答复  
会员登录
获取验证码
登录
登录
我的资料
留言
回到顶部