目录
接口构造
秒杀数量放在缓存,然后进行秒杀,数量减少成功之后,将用户信息放在阻塞队列,使用mq去拿到队列元素发送消息进行数据库添加秒杀记录
第一次尝试
使用redisTemplate INCR函数进行实现
当INCR的结果超过商品数量的时候不让用户秒杀
double result = redisTemplate.opsForValue().increment("1", 1);
//商品数量为1500
if (result > 1500) {
return -1;
} else {
try {
arrayBlockingQueue.put(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return 1;
redis商品数量初始化的时候为0 ,每次秒杀加一
使用CountDownLoad进行模拟并发操作
CountDownLatch countDownLatch=new CountDownLatch(150);
150个并发,2000多个请求量,存放到队列
static ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue(100);
@Override
public Integer shop(String name) {
double result = redisTemplate.opsForValue().increment("1", 1);
if (result > 1500) {
return -1;
} else {
try {
arrayBlockingQueue.put(name);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return 1;
}
package com.example.demo;
import java.util.concurrent.CountDownLatch;
/**
* Created on 2019/5/7.
*
* @author yangsen
*/
public class MyThread implements Runnable {
Service service;
private String i;
CountDownLatch latch;
@Override
public void run() {
service.shop("dajitui" + i);
latch.countDown();
}
public MyThread(String i, CountDownLatch latch,Service service) {
this.service=service;
this.latch = latch;
this.i = i;
}
}
最后执行到mq发送大概在0.5秒,相当快
github地址https://github.com/dajitui/shop
sql
CREATE TABLE `shop` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) CHARACTER SET utf8mb4 DEFAULT NULL,
`count` int(11) DEFAULT NULL,
`point` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=667 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_bin;
再次改进
当并发量到达1w的时候,报出redis连接数不够,所以我们对程序再次改写
@RequestMapping("/shop")
public void shop() throws InterruptedException {
long time=System.nanoTime();
for(int i=0;i<10009;i++){
new Thread(new Runnable() {
@Override
public void run() {
IServiceImpl.concurrentLinkedQueue.offer("1");
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
System.out.println(IServiceImpl.concurrentLinkedQueue.isEmpty());
while (true){
if(!IServiceImpl.concurrentLinkedQueue.isEmpty()){
double result = redisTemplate.opsForValue().increment("1", 1);
if (result > 10009) {
break;
} else {
System.out.println("抢购"+atomicLong.incrementAndGet());
}
}
}
long time1=System.nanoTime();
System.out.println("使用increment消耗时间"+(time1-time));
}
public static ConcurrentLinkedQueue<String> concurrentLinkedQueue = new ConcurrentLinkedQueue();
先把所有请求放在一个无界队列里面,然后再平稳的输出,再使用redis进行INCR操作,其实还可以再优化的,就是当一个因为数量不足失败之后,后面的可以都标识为失败。后续如果说支付不成功,我们再增加库存。
浏览器先访问http://localhost:8080/start进行redis数量重置,访问http://localhost:8080/shop进行操作,模拟1w并发,最后执行完大概在7秒多
github地址https://github.com/dajitui/shop1
第二次尝试
使用redis分布锁,每次请求都获取锁,得到锁再操作,没有的话循环去请求锁...
结果很蛋疼,因为变成了单线程去处理秒杀了(分布锁),虽然redis是单线程的,但是人家INCR函数使用时间肯定比你获取锁,执行本地操作,释放锁快,所以分布锁尝试失败
第三次试想
一般逻辑都是去减库存,所以我们可以使用lua脚本来控制原子性操作,redis执行lua脚本,查询数量之后如果符合数量>0原子性将它数量减一。
这个目前没有完成,大家可以在评论中提供参考路径哦,让我学习学习
最近看了一个github秒杀接口的项目:
大概使用了很多个方法:
1.悲观锁
2.乐观锁
3.jvm锁(ReentranLock)
4.AOP+jvm锁
5.进程队列然后再进行消费
6.Disruptor方法
https://gitee.com/52itstyle/spring-boot-seckill
个人看法:1,2点只适合小型并发,扛不住大型并发。第三种据作者反应会出现超卖的情况,可能是事务还没提交,就把锁释放导致的。而第四种可以解决第3种的问题。第5种其实在我个人的第一次尝试的改进使用了,效果算不错。第6种后面再讲。
第四次
这一次是看别人的代码,上菜(上面的AOP+jvm锁)
@Servicelock
@Component
@Scope
@Aspect
@Order(1)
//order越小越是最先执行,但更重要的是最先执行的最后结束。order默认值是2147483647
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();
throw new RuntimeException();
} finally{
lock.unlock();
}
return obj;
}
}
其实也没有什么。就是在程序的开始还有结束获取锁,还有释放锁。
第5次
重量级出场Disruptor
这个我也是第一次接触
Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中
他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟
产生大量交易。这个系统是建立在JVM平台上,其核心是一个业务
逻辑处理器,它能够在一个线程里每秒处理6百万订单。业务逻辑处
理器完全是运行在内存中,使用事件源驱动方式。业务逻辑处理器
的核心是Disruptor。
大家可以看下Disruptor具体的实现,依赖RingBuffer环形的数据结构,Disruptor通过生成者还有消费者去获取共享数据。
为啥他如此优秀?这里只简单说下,它是一个无锁的队列,每次新增都会添加一个版本号,使用了cas去控制并发。好像牛逼的都是在数据结构还有使用cas实现的,比如concurrent
个人从之前的github项目里面抽出这一部分进行测试性能
https://github.com/dajitui/Disruptor
创建了一个生成者,一个消费者,1w并发,处理完是在120秒
总结
拿第一种跟第5种比较,第5种启动之后cpu很高,而且执行的时间比第一种还长。我们可以使用第一种来进行后续的改进,第5种对系统要求相当高