略
秒杀系统高并发优化
高并发发生在哪?
详情页
为什么要单独获取系统时间?
等待秒杀时,用户会大量刷新页面**(解决::使用CDN,detail页面静态化,静态资源css,js)**,此时的时间就需要通过请求来获取了.
CDN是个啥?
- (内容分发网络)加速用户获取数据的系统;
- 部署在离用户最近的网络节点;
- 命中CDN后不用访问后端服务器;
- 互联网公司自己搭建或租用CDN;
获取系统时间不用优化
java访问一次内存(Cacheline)大约10ns,特别小,一秒一亿次,获取系统时间没有任何后端访问,仅仅是new Date()
,然后输出;
秒杀地址接口
- 无法使用CDN缓存,随着时间接口是变化的(接口从不可访问,到可以访问,再到不可访问的变化,即等秒杀,秒杀,过期);
- 适合服务器端缓存:如redis等,可以抗一秒10万次的qps,还可以做集群,可以抗百万次的qps;
- 一致性维护成本低,通过业务代码很容易同步数据库和缓存里的数据;
优化: 先查询redis,如果有数据就从redis中取得,如果没有就从mysql中取,然后保存到redis中,下一次访问就从redis中取,redis中还有限制时间的配置,例如:配置半小时后redis中的数据过期,过期后直接穿透redis来访问mysql,或者mysql更新时主动更新redis;
秒杀操作
问题
- 无法使用CDN,因为它存在写操作
- 后端缓存困难:库存问题
- 一行数据竞争:热点商品
优化
- 执行秒杀
- 原子计数器 实现减库存 使用redis等Nosql
- 记录行为消息 实现消息转移 使用分布式MQ
- 消费消息并落地 实现数据保存 使用mysql
- 结束
优点:可以抗很高并发,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更新库存成功
- 不出错
- 客户端确认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
| <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) { Seckill seckill = redisDao.getSeckill(seckillId); if (seckill == null) { seckill = seckillDao.queryById(seckillId); if (seckill == null) { return new Exposer(false, seckillId); } else { 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()); } 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) { throw new SeckillCloseException("seckill is closed"); } else { SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone); return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled); } }
|
深度优化
使用存储过程:
- 减少事务行级锁持有时间减少
- 不要过度使用存储过程
- 简单的逻辑可以使用存储过程
- QPS:一秒杀单6000/qps
不会写
大型系统部署架构
- CDN(内容分发网络)
- WebServer: Nginx + Jetty
- Redis
- 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[统计分析]
|