前言

在单体架构的秒杀活动中,为了减轻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构建分布式秒杀系统

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

  1. 从构建分布式秒杀系统聊聊Disruptor高性能队列

    前言 秒杀架构持续优化中,基于自身认知不足之处在所难免,也请大家指正,共同进步.文章标题来自码友 简介 LMAX Disruptor是一个高性能的线程间消息库.它源于LMAX对并发性,性能和非阻塞算法 ...

  2. 从构建分布式秒杀系统聊聊验证码 给大家推荐8个SpringBoot精选项目

    前言 为了拦截大部分请求,秒杀案例前端引入了验证码.淘宝上很多人吐槽,等输入完秒杀活动结束了,对,结束了...... 当然了,验证码的真正作用是,有效拦截刷单操作,让羊毛党空手而归. 验证码 那么到底 ...

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

    秒杀架构到后期,我们采用了消息队列的形式实现抢购逻辑,那么之前抛出过这样一个问题:消息队列异步处理完每个用户请求后,如何通知给相应用户秒杀成功? 场景映射 首先,我们举一个生活中比较常见的例子:我们去 ...

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

    前言 ​最近,被推送了不少秒杀架构的文章,忙里偷闲自己也总结了一下互联网平台秒杀架构设计,当然也借鉴了不少同学的思路.俗话说,脱离案例讲架构都是耍流氓,最终使用SpringBoot模拟实现了部分秒杀场 ...

  5. 使用.net core构建分布式SAAS系统(目录)

    一 前言 二 项目背景 三 项目架构-从单体应用到微服务 四 大数据量下的分库分表 五 缓存处理--进程内缓存与Redis的使用 六 使用MNS队列来流量削峰 七 百万Job的任务调度系统 八 每天1 ...

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

    问题导读 1.ELK产生的背景?2.ELK的基本组成模块以及各个模块的作用?3.ELK的使用总计有哪些? 背景 日志,对每个系统来说,都是很重要,又很容易被忽视的部分.日志里记录了程序执行的关键信息, ...

  7. Redis分布式锁----悲观锁实现,以秒杀系统为例

    摘要:本文要实现的是一种使用redis来实现分布式锁. 1.分布式锁 分布式锁在是一种用来安全访问分式式机器上变量的安全方案,一般用在全局id生成,秒杀系统,全局变量共享.分布式事务等.一般会有两种实 ...

  8. springboot项目:Redis分布式锁的使用(模拟秒杀系统)

    模拟秒杀系统: 第一步:编写Service package com.payease.service; /** * liuxiaoming * 2017-12-14 */ public interfac ...

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

    本文使用redis来实现乐观锁,并以秒杀系统为实例来讲解整个过程. 乐观锁      大多数是基于数据版本(version)的记录机制实现的.即为数据增加一个版本标识,在基于数据库表的版本解决方案中, ...

随机推荐

  1. prometheus 基于文件的目标发现

    prometheus 基于文件的目标发现 1.创建目录 cd /usr/local/prometheus/conf mkdir -pv targets/{nodes,docker} 2.修改prome ...

  2. Python之False和None

    这个其实在Python文档当中有写了,为了准确起见,我们先引用Python文档当中的原文: In the context of Boolean operations, and also when ex ...

  3. openJDK之如何下载各个版本的openJDK源码

    如果我们需要阅读openJDK的源码,那么需要下载,那么该去哪下载呢? 现在JDK已经发展到版本10了,11已经处于计划中,如果需要特定版本的openJDK,它们的下载链接在哪呢? 1.openJDK ...

  4. 关于微信登录授权获取unionid的方法

    前言:微信登录授权是目前普遍存在于小程序的,还有一种静默授权方式是微信提供的但是不推荐使用,由于不同设备登录openid是不同的那么我们应该怎样拿到一个唯一的ID呢,下面做分享 wxml代码 < ...

  5. MySql实现分页查询的SQL,mysql实现分页查询的sql语句 (转)

    http://blog.csdn.net/sxdtzhaoxinguo/article/details/51481430 摘要:MySQL数据库实现分页查询的SQL语句写法! 一:分页需求: 客户端通 ...

  6. [再寄小读者之数学篇](2014-06-23 Hardy 空间、BMO空间与 Triebel-Lizorkin 空间)

    $$\bex 0<p<\infty\ra H_p=\dot F^0_{p,2};\quad BMO=\dot F^0_{\infty,2}. \eex$$ see [H. Triebel, ...

  7. BZOJ 5093: [Lydsy1711月赛]图的价值

    第二类斯特林数模版题 需要一些组合数的小$ trick$ upd:这里更新了本题巧妙的$ O(k)$做法,虽然常数很大就是了 传送门:here 题意:求所有$ n$个节点的无重边自环图的价值和,定义一 ...

  8. phpstudy 2016 切换Nginx+php7.0版本所需运行库 vc14 + 安装redis拓展

    去微软官方下载vc14的运行库 链接:https://www.microsoft.com/en-us/download/details.aspx?id=48145 32位运行库 安装成功 切换版本成功 ...

  9. php in_array() 循环大量数组时效率特别慢问题

    in_array() 会循环数组内部元素逐个匹配,特别耗时,换成以下方式,效率大大提升

  10. DCM、PLL、PMCD、MMCM相关

    摘自网上 : http://xilinx.eetop.cn/viewnews-1482 The DCM is a Digital Clock Manager - at its heart it is ...