在单体架构的秒杀活动中,为了减轻DB层的压力,这里咱们采用了Lock锁来实现秒杀用户排队抢购。然而很不幸的是尽管使用了锁,可是测试过程当中仍然会超卖,执行了N屡次发现依然有问题。输出一下代码吧,可能你们看的比较真切:html
@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(); //这里、不清楚为啥、老是会被超卖10一、难道锁不起做用、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(),输出结果没问题。因为还有其余事情要作,最终仍是带着疑问提交代码到码云。java
若是想分享代码并使你们一块儿参与进来,必定要自荐,这样才会被更多的人发现。固然,若是有交流群必定要留下联系方式,这样讨论起来可能更方便。项目被推荐后,果真加群的小伙伴就多了。因为项目配置好相应参数就能够测试,而且每一个点都有相应的文字注释,其中有心的小伙伴果真注意到了我写的注释<这里、不清楚为啥、老是会被超卖10一、难道锁不起做用、lock是同一个对象>,而后提出了困扰本身好多天的问题。git
码友zoain说,测试了很久终于发现了问题,原来lock锁是在事物单元中执行的。看到这里,小伙伴们有没有恍然大悟,反正我是悟了。这里,总结一下为何会超卖101:秒杀开始后,某个事物在未提交以前,锁已经释放(事物提交是在整个方法执行完),致使下一个事物读取到了上个事物未提交的数据,也就是传说中的脏读。此处给出的建议是锁上移,也就是说要包住整个事物单元。spring
为了包住事物单元,这里咱们使用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 里的切片在未指定排序的时候,两个注解是随意执行的。若是事务在加锁前执行的话,是否是就会产生问题?ide
首先,因为本身实在没有时间去取证,最终仍是码友IM核米完成了自问自答,这里引用下他的解释:spring-boot
我说的没错,但 @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
为何没有用synchronized?
代码案例:从0到1构建分布式秒杀系统