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

非盈利平台

只为分享一些优质内容

Java帮帮-微信公众号

Java帮帮-微信公众号

将分享做到极致

微信小程序

微信小程序

更方便的阅读

百度小程序

百度小程序

搜索便捷阅读

支付宝小程序

支付宝小程序

支付也能阅读

程序员生活志-公众号

程序员生活志-公众号

程序员生活学习圈,互联网八卦黑料

支付宝赞助-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帮帮生态承诺

一直坚守,不负重望

初心
勤俭
诚信
正义
分享
友链交换:加帮主QQ2524138991 留言即可 24小时内答复  
满吉教育资讯
会员登录
获取验证码
登录
登录
我的资料
留言
回到顶部