基于SSM实现高并发秒杀API(高并发)

秒杀系统高并发优化

高并发发生在哪?

详情页

为什么要单独获取系统时间?

等待秒杀时,用户会大量刷新页面**(解决::使用CDN,detail页面静态化,静态资源css,js)**,此时的时间就需要通过请求来获取了.

CDN是个啥?
  1. (内容分发网络)加速用户获取数据的系统;
  2. 部署在离用户最近的网络节点;
  3. 命中CDN后不用访问后端服务器;
  4. 互联网公司自己搭建或租用CDN;
获取系统时间不用优化

java访问一次内存(Cacheline)大约10ns,特别小,一秒一亿次,获取系统时间没有任何后端访问,仅仅是new Date(),然后输出;

秒杀地址接口
  1. 无法使用CDN缓存,随着时间接口是变化的(接口从不可访问,到可以访问,再到不可访问的变化,即等秒杀,秒杀,过期);
  2. 适合服务器端缓存:如redis等,可以抗一秒10万次的qps,还可以做集群,可以抗百万次的qps;
  3. 一致性维护成本低,通过业务代码很容易同步数据库和缓存里的数据;

优化: 先查询redis,如果有数据就从redis中取得,如果没有就从mysql中取,然后保存到redis中,下一次访问就从redis中取,redis中还有限制时间的配置,例如:配置半小时后redis中的数据过期,过期后直接穿透redis来访问mysql,或者mysql更新时主动更新redis;

秒杀操作

问题
  1. 无法使用CDN,因为它存在写操作
  2. 后端缓存困难:库存问题
  3. 一行数据竞争:热点商品
优化
  1. 执行秒杀
  2. 原子计数器 实现减库存 使用redis等Nosql
  3. 记录行为消息 实现消息转移 使用分布式MQ
  4. 消费消息并落地 实现数据保存 使用mysql
  5. 结束

优点:可以抗很高并发,redis和mq都可以支持很高的qps
缺点:运维成本和稳定性:NoSQL和MQ等;开发成本:数据一致性和回滚方案等;幂等性保证:重复秒杀问题;不适合新手:比较复杂
运用:抢红包

为什么不用mysql解决?mysql低效?真的低效吗?测试后发现一条update可以有约4万QPS的压力(一个商品一秒可以支持4万次)

Java控制事务的分析

对于同一个商品的减库存,当A用户update并且insert时,它会锁住该行(该商品),其他用户B,C,D等处于等待,等ACommit或者RollBack后,其他用户才能获得锁,然后B开始Update,Insert并加锁,然后C,D,E等处于等待状态.很明显,这样串行化的操作会让后面的用户等待相当长的一段时间,因为他要等前面所有的用户都释放掉锁才会执行.

瓶颈分析:

update到insert到commit/rollback , 这里面每个过程中都存在网络延迟和java的GC操作 ,都会耗费想当多的时间.所以啊!!这个锅,咱mysql不背.

行级锁在Commit/RollBack后释放,所以重点在如何减少行级锁持有时间

延迟问题很关键,经测试,同城机房网络:(0.5ms-2ms) max(1000qps),update后JVM的GC(50ms) max(20qps).如果出现一次GC,况且还很频繁出现,那就等的久了,累计下来,最后个用户要等到不耐烦的.跟何况还可能是异地访问(20ms).

判断Update更新库存成功

  1. 不出错
  2. 客户端确认Update影响记录数

*优化思路: 把客户端逻辑放到MySQL服务器上,避免网络延迟和GC影响
优化方案: 1.定制SQL方案,修改MySQL源码(技术要6,大公司搞的)例如update后面加auto_commit(当记录数为1的时候不会滚,为其他就回滚);2.存储过程,服务器端控制事务

总结
  • 前端控制-暴露接口,按钮防重复
  • 动静态数据缓存-CDN缓存,后端缓存
  • 事务竞争优化-减少事务锁时间

开始优化

spring-dao.xml添加

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- redis -->
<bean id="redisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="300"/>
<property name="maxWaitMillis" value="3000"/>
<property name="testOnBorrow" value="true"/>
</bean>
<bean id="redisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="localhost"/>
<property name="port" value="6379"/>
<property name="database" value="0"/>
<property name="password" value="root"/>
<property name="poolConfig" ref="redisPoolConfig"/>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
<property name="connectionFactory" ref="redisConnectionFactory"/>
<!--序列化-->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer"/>
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
</property>
</bean>

RedisDao.java编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Repository
public class RedisDao {
@Autowired
private RedisTemplate<String, Seckill> redisTemplate;
public Seckill getSeckill(long seckillId) {
Seckill seckill = null;
String key = "seckill:" + seckillId;
seckill = redisTemplate.boundValueOps(key).get();
return seckill;
}
public void setSeckill(Seckill seckill) {
String key = "seckill:" + seckill.getSeckillId();
redisTemplate.boundValueOps(key).set(seckill, 1, TimeUnit.HOURS);
}
}

exportSeckillUrl()修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Override
public Exposer exportSeckillUrl(long seckillId) {
//通过Redis进行缓存 使用超时来维护redis
//1. 访问redis
Seckill seckill = redisDao.getSeckill(seckillId);
//如果不为空,就跳过数据库查找走Redis
if (seckill == null) {
//访问数据库
seckill = seckillDao.queryById(seckillId);
if (seckill == null) {
//数据库查不到
return new Exposer(false, seckillId);
} else {
//查询到了放入redis
redisDao.setSeckill(seckill);
}
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
Date nowTime = new Date();
if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
return new Exposer(false, seckillId, nowTime.getTime(), endTime.getTime());
}
//Md5转换为字符串,不可逆
String md5 = getMD5(seckillId);
return new Exposer(true, md5, seckillId);
}

并发优化

缩短行级锁时间

insert操作可以提前进行,然后再update,逻辑部分修改为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//记录购买记录
int insertCount = successKilledDao.insertSucceccKilled(seckillId, userPhone);

if (insertCount <= 0) {
//重复秒杀
throw new RepeatKillException("seckill repeated");
} else {
//减库存
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//rollback
throw new SeckillCloseException("seckill is closed");
} else {
//Commit
//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
}
}

深度优化

使用存储过程:

  1. 减少事务行级锁持有时间减少
  2. 不要过度使用存储过程
  3. 简单的逻辑可以使用存储过程
  4. QPS:一秒杀单6000/qps

不会写

大型系统部署架构

  1. CDN(内容分发网络)
  2. WebServer: Nginx + Jetty
  3. Redis
  4. MySQL

细说

1
2
3
4
5
6
7
graph LR
A[流量]-->|用到| B[CDN缓存]
A-->C[智能DNS解析Nginx]
C-->D[逻辑集群Jetty]
D-->|用到| E[缓存集群redis]
D-->F[分库分表DB1,DB2..]
F-->G[统计分析]