• 从构建分布式秒杀系统聊聊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

    总结

    https://www.cnblogs.com/smallSevens/p/9114734.html

  • 相关阅读:
    收集的各种学习链接,方便查找
    用python调试Appium和雷电模拟器连接时出现Original error: Could not find 'adb.exe' in PATH
    App自动化测试之Appium环境安装(涉及雷电模拟器和真机)
    元素定位工具ChroPath
    Python+Selenium学习笔记19
    Python+Selenium学习笔记18
    Python+Selenium学习笔记17
    Python+Selenium学习笔记16
    Python+Selenium学习笔记14
    Python+Selenium学习笔记15
  • 原文地址:https://www.cnblogs.com/smallfa/p/13141135.html
Copyright © 2020-2023  润新知