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

前言

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

@Service("seckillService")
public class SeckillServiceImpl implements ISeckillService {
    /**
     * 思考:为什么不用synchronized
     * service 默认是单例的,并发下lock只有一个实例
     */
    private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
    @Autowired
    private DynamicQuery dynamicQuery;

    @Override
    @Transactional
    public 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)
@Documented
public  @interface Servicelock {
     String description()  default "";
}

自定义切面LockAspect:

@Component
@Scope
@Aspect
public class LockAspect {
    /**
     * 思考:为什么不用synchronized
     * service 默认是单例的,并发下lock只有一个实例
     */
    private static  Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁  

    //Service层切点     用于记录错误日志
    @Pointcut("@annotation(com.itstyle.seckill.common.aop.Servicelock)")
    public void lockAspect() {

    }

    @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")
public class SeckillServiceImpl implements ISeckillService {
    /**
     * 思考:为什么不用synchronized
     * service 默认是单例的,并发下lock只有一个实例
     */
    private Lock lock = new ReentrantLock(true);//互斥锁 参数默认false,不公平锁
    @Autowired
    private DynamicQuery dynamicQuery;

    @Override
    @Servicelock
    @Transactional
    public 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锁同志,很有可能是你使用的方式不对

思考

为什么没有用synchronized?

代码案例:从0到1构建分布式秒杀系统

原文地址:http://blog.51cto.com/itstyle/2124865

时间: 2024-10-11 20:42:03

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

从构建分布式秒杀系统聊聊限流的多种实现

前言 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的.两周前秒杀案例初步成型,分享到了中国最大的同×××友网站-码云.同时也收到了不少小伙伴的建议和投诉.我从不认为分布式.集群.秒杀这些就应该是大厂的专利,在互联网的今天无论什么时候都要时刻武装自己,只有这样,也许你的春天就在明天. 在开发秒杀系统案例的过程中,前面主要分享了队列.缓存.锁和分布式锁以及静态化等等.缓存的目的是为了提升系统访问速度和增强系统的处理能力:分布式锁解决了集群下数据的安全一致性问题:静态化无疑

从构建分布式秒杀系统聊聊限流特技

前言 俗话说的好,冰冻三尺非一日之寒,滴水穿石非一日之功,罗马也不是一天就建成的.两周前秒杀案例初步成型,分享到了中国最大的同性交友网站-码云.同时也收到了不少小伙伴的建议和投诉.我从不认为分布式.集群.秒杀这些就应该是大厂的专利,在互联网的今天无论什么时候都要时刻武装自己,只有这样,也许你的春天就在明天. 在开发秒杀系统案例的过程中,前面主要分享了队列.缓存.锁和分布式锁以及静态化等等.缓存的目的是为了提升系统访问速度和增强系统的处理能力:分布式锁解决了集群下数据的安全一致性问题:静态化无疑是

从构建分布式秒杀系统聊聊分布式锁

前言 最近懒成一坨屎,学不动系列一波接一波,大多还都是底层原理相关的.上周末抽时间重读了周志明大湿的 JVM 高效并发部分,每读一遍都有不同的感悟.路漫漫,借此,把前段时间搞着玩的秒杀案例中的分布式锁深入了解一下. 案例介绍 在尝试了解分布式锁之前,大家可以想象一下,什么场景下会使用分布式锁? 单机应用架构中,秒杀案例使用ReentrantLcok或者synchronized来达到秒杀商品互斥的目的.然而在分布式系统中,会存在多台机器并行去实现同一个功能.也就是说,在多进程中,如果还使用以上JD

从构建分布式秒杀系统聊聊验证码

前言 为了拦截大部分请求,秒杀案例前端引入了验证码.淘宝上很多人吐槽,等输入完秒杀活动结束了,对,结束了...... 当然了,验证码的真正作用是,有效拦截刷单操作,让羊毛党空手而归. 验证码 那么到底什么是验证码呢?验证码作为一种人机识别手段,其终极目的,就是区分正常人和机器的操作.我们常见的互联网注册.登录.发帖.领优惠券.投票等等应用场景,都有被机器刷造成各类损失的风险. 目前常见的验证码形式多为图片验证码,即数字.字母.文字.图片物体等形式的传统字符验证码.这类验证码看似简单易操作,但实际

从构建分布式秒杀系统聊聊为什么不用synchronized

前言 技术没有高低之分,适合自己的就是最好的.只有努力扩展自己的知识边界,才能探索更多未知领域. 案例 在分析为什么不用 synchronized 这个问题之前,我们先用代码说话,LockDemo 测试案例: /** * 案例测试 * @author */ public class LockDemo { private static Lock lock = new ReentrantLock(); private static int num1 = 0; private static int n

从构建分布式秒杀系统聊聊WebSocket推送通知

前言 秒杀架构到后期,我们采用了消息队列的形式实现抢购逻辑,那么之前抛出过这样一个问题:消息队列异步处理完每个用户请求后,如何通知给相应用户秒杀成功? 场景映射 首先,我们举一个生活中比较常见的例子:我们去银行办理业务,一般会选择相关业务打印一个排号纸,然后就可以坐在小板凳上玩着手机,等待被小喇叭报号.当小喇叭喊到你所持有的号码,就可以拿着排号纸去柜台办理自己的业务. 这里,假设当我们取排号纸的时候,银行根据时间段内的排队情况,比较人性化的提示用户:排队人数较多,您是否继续等待?否的话我们可以换

SpringBoot开发案例从0到1构建分布式秒杀系统

前言 最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路.俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场景,同时跟大家分享交流一下. 秒杀场景 秒杀场景无非就是多个用户在同时抢购一件或者多件商品,专用词汇就是所谓的高并发.现实中经常被大家喜闻乐见的场景,一群大妈抢购打折鸡蛋的画面一定不会陌生,如此场面让服务员大姐很无奈,赶上不要钱了. 业务特点 瞬间高并发.电脑旁边的小哥哥.小姐姐们如超市哄抢的大妈一般

利用开源架构ELK构建分布式日志系统

本文介绍了如何使用成熟的经典架构ELK(即Elastic search,Logstash和Kibana)构建分布式日志监控系统,很多公司采用该架构构建分布式日志系统,包括新浪微博,freewheel,畅捷通等. 背景日志,对每个系统来说,都是很重要,又很容易被忽视的部分.日志里记录了程序执行的关键信息,ERROR和WARNING信息等等.我们可以根据日志做很多事情,做数据分析,系统监控,排查问题等等 .但是,任何一个中大型系统都不可能是单台Server,日志文件散落在几十台甚至成千上万台Serv

Redis分布式锁----乐观锁的实现,以秒杀系统为例

本文使用redis来实现乐观锁,并以秒杀系统为实例来讲解整个过程. 乐观锁      大多数是基于数据版本(version)的记录机制实现的.即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现读取出数据时,将此版本号一同读出,之后更新时,对此版本号加1.此时,将提交数据的版本号与数据库表对应记录的当前版本号进行比对,如果提交的数据版本号大于数据库当前版本号,则予以更新,否则认为是过期数据.redis中可以使用watch命令