秒杀系统⾼并发优化
通过该篇⽂章可以学习到:
————————————————————————————————
1 ⾼并发系统优化思路分析
2 ⾼并发优化技巧
动静态资源分离:CDN(内容发布⽹络):缓存静态资源
Redis:缓存动态资源
并发优化:
SQL优化降低⾏级锁持有时间
存储过程优化降低⾏级锁持有时间
3 集化部署
—
———————————————————————————————
源码参照:
1 优化分析
结合该⾼并发系统考虑,哪些是可能出现的⾼并发点呢?
上图中,所有的红⾊的部分都可能是出现⾼并发的点。
1.1为什么单独获取系统时间
在详情页,可能出现⽤户⼤量的刷新情况,此时系统应该部署在CDN节点上,此时要做⼀个静态化处理,当再次刷新时它获取的CDN 静态资源(css/js/picture),但是,时间要保持实时的,所以要单独的做⼀个处理,单独从服务器系统上获取时间,这也就是为什么要在详情页单独获取系统时间了。
1.2 CDN是什么
简介:CDN(内容发布⽹络),是⼀个加速⽤户获取数据的系统;既可以是静态资源,⼜可以是动态资源,这取决于我们的决策策略。经常⼤部分视频加速都依赖于CDN,⽐如优酷,爱奇艺等,据此加速;
原理:CDN部署在距离⽤户最近的⽹络节点上,⽤户上⽹的时候通过⽹络运营商(电信,长城等)访问距离⽤户最近的要给城域⽹⽹络地址节点上,然后通过城域⽹跳到主⼲⽹上,主⼲⽹则根据访问IP到访问资源所在服务器,但是,很⼤⼀部分内容在上⼀层节点已经到,此时不⽤往下继续查,直接返回所访问的资源即可,减⼩了服务器的负担。⼀般互联⽹公司都会建⽴⾃⼰的CDN机或者租⽤CDN。
1.3 获取系统时间不⽤优化
获取系统访问时间的操作不⽤优化,因为访问⼀次内存Cacheline⼤约10ns,1秒内可以做很⼤数据量级的时间获取操作,所以,不⽤做什么优化!
1.4 秒杀地址(Redis缓存技术)
对于秒杀地址暴露的接⼝是否可以缓存呢?
秒杀接⼝是⽆法缓存在CDN当中的,因为CDN适合缓存不易变化的资源,通常是静态资源,⽐如css/jquery资源,每⼀个url对应了⼀个不变的内容,秒杀的接⼝地址是在每次都发⽣变化的,不适合放在CDN缓存。
但是适合放在服务器端做缓存(后端缓存),⽐如redis等,下⼀次访问的时候直接去服务器端缓存⾥⾯查,如果服务器端缓存有了就直接拿⾛,没有的话再做正常数据访问处理;另外⼀个原因就是,⼀致性维护成本很低。
秒杀地址接⼝的优化策略:
请求地址,先访问redis,如果redis缓存中没有所需资源或者访问访问超时,则直接进⼊mysql获取系统资源,将获取的内容更新在redis当中(策略:超时穿透,主动更新)。
1.5 秒杀操作
1.5.1 秒杀操作分析
(a)秒杀操作优化分析
对于这种写操作,是⽆法使⽤CDN优化的,另外,也不适合在后端缓存,因为缓存了其他数据,可能会出现数据不⼀致的情况。
秒杀数据数据操作的⼀个困难的点就是⼀⾏数据⼤量⽤户出现竞争的情况,同时出现⼤量的(b)update操作,这样该如何优化呢?
(架构+维护点)
设计⼀个原⼦计数器(redis/NoSQL来实现)⽤来记录⾏为信息(⽤分布式MQ实现这个消息队列,即把消息放在MQ当中),然后后端服务消费此消息并落地(⽤Mysql实现,落地:记录购买者,能够扛着很⼤的访问量)
停止快捷但是这个⽽技术的有⾃⼰的弱点,也就是成本⽅⾯:
运维成本和稳定型:NoSQL,MQ等;开发成本在数据⼀致性和回滚⽅案等;幂等性难以保证:重复秒杀的问题;不适合新⼿的架构。
(c)为什么不⽤MySql来解决秒杀操作?
因为Mysql执⾏update的减库存⽐较低效,⼀条update操作的压⼒测试结果是可以抗住4wQPS,也就是说,⼀个商品在1秒内,可以被买4w次;
看⼀下Java控制事务的⾏为分析:
(执⾏库存减1操作)
Update table set num=num-1 where id=10 andnum>0,紧接着会进⾏⼀个inser购买明细的操作,然后commit/rollback;
然后第⼆个⽤户Updatetable set num=num-1 where id=10 and num>0,紧接着等待⾏锁,获得锁lock,来继续执⾏,然后后⾯的⽤户……
这样下来的话,整个秒杀操作可以说是⼀种串⾏化的执⾏序列。
1.5.2 分析瓶颈所在
Update减库存—>insert购买明细—>commit/rollback:这两个过程都存在⽹路延迟和GC;但并⾮java和sql本⾝慢,⽽是java和通信之间⽐较慢;
所以,java执⾏时间+⽹络延迟时间+GC=这⾏操作的执⾏时间(⼤概在2ms,1秒钟有500次减操作,对于秒杀系统来说这个性能呈指数级下降,并不好)。
1.5.3 优化思路分析
我们知道⾏级锁是在commit之后释放的,那么我们的优化⽅向就是减少⾏级锁的持有时间。
同城机房需要花0.5-2msmax(1000qps),update之后JVM-GC(50ms) max(20qps);
异地机房⼀次(北京上海之间额⼀次update Sql需要20ms。
如何判断update更新库存成功?
两个条件:——Update⾃⾝不报错,客户端确认影响记录数
优化思路:
把客户端逻辑放在Mysql服务端,避免⽹络延迟和GC影响。
那么,如何把逻辑放在Mysql服务端呢?
1.5.4 两种优化解决⽅案
(1)定制SQL⽅案:update/*+[auto_commit]*/,需要修改Mysql源码;这样可以在SQL执⾏完之后,直接在服务端完成commit,不⽤客户端逻辑判断之后来执⾏是否commit/rollback。 但是这个增加了修改Mysql源码的成本(不推荐)。
(2)使⽤存储过程:整个事务在MySQL端完成(把整个热点执⾏放在⼀个过程当中⼀次性完成,只需要返回执⾏的整个结果就⾏了,这样可以避免⽹络延迟和GC⼲扰)。
1.6 优化分析总结
前端控制:暴露接⼝(动静态数据分离)
按钮防重复(避免重复请求)
动静态数据分离:CDN缓存,后端缓存(redis技术实现的查询)。
事务竞争优化:减少事务锁时间(⽤Mysql来解决)。
2 Redis后端缓存优化
2.1 Redis 安装
Redis在通常情况下都是使⽤机来维护缓存,此处⽤⼀个Redis缓存为例。
此处应⽤的⽬的:使⽤redis优化地址接⼝,暴露接⼝。
若想使⽤Redis作为服务端的缓存机制,则应该⾸先在服务端安装Redis:具体安装教程
2.2 优化编码
第⼀,在l⽂件引⼊Redis在Java环境下的客户端Jedis.
<!--⾼并发优化:Redis在java环境中的客户端Jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
<!-- protostuff序列化依赖,写⾃⼰的序列化⽅式-->
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-core</artifactId>
<version>1.0.8</version>
</dependency>
<dependency>
<groupId>com.dyuproject.protostuff</groupId>
<artifactId>protostuff-runtime</artifactId>
<version>1.0.8</version>
</dependency>
第⼆,添加⼀个对象序列化的缓存类RedisDao.java:
为什么要使⽤对象序列化?
序列化的⽬的是将⼀个实现了Serializable接⼝的对象转换成⼀个字节序列,可以。 把该字节序列保存起来(例如:保存在⼀个⽂件⾥),以后可以随时将该字节序列恢复为原来的对象。
序列化的对象占原有空间的⼗分之⼀,压缩速度可以达到两个数量级,同时节省了CPU
Redis 缓存对象时需要将其序列化,⽽何为序列化,实际上就是将对象以字节形式存储。这样,不管对象的属性是字符串、整型还是图⽚、视频等⼆进制类型,
都可以将其保存在字节数组中。对象序列化后便可以持久化保存或⽹络传输。需要还原对象时,只需将字节数组再反序列化即可。
public class RedisDao {
private final Logger Class());
private final JedisPool jedisPool;
public RedisDao(String ip,int port)
{
//⼀个简单的配置;
jedisPool=new JedisPool(ip,port);
}
// protostuff序列化⼯具⽤到的架构
// 可以⽤RuntimeSchema来⽣成schema(架构:序列化模式)通过反射在运⾏时缓存和使⽤
private RuntimeSchema<Seckill> ateFrom(Seckill.class);
public Seckill getSeckill(long seckillId)
{transport海词
//缓存Redis操作逻辑,⽽不应该放在Service下,因为这是数据访问层的逻辑
try{
Jedis Resource();
try{
String key="seckill:"+seckillId;
//并没有实现内部序列化操作
//get->byte[]->反序列化-》Object(Seckill)
//采⽤⾃定义序列化⽅式
//采⽤⾃定义的序列化,在l⽂件中引⼊两个依赖protostuff:pojo
byte[] (Bytes());
//重新获取缓存
if(bytes!=null)
{
Seckill wMessage();
//将bytes按照从Seckill类创建的模式架构schema反序列化赋值给对象seckill;
//将bytes按照从Seckill类创建的模式架构schema反序列化赋值给对象seckill;
//Seckill被反序列化
return seckill;
}
}
finally{
jedis.close();
jquery下拉菜单编程
}
}
catch(Exception e){
<(e.getMessage(),e);
}
return null;
}
public String putSeckill(Seckill seckill)
{
//set Object(Seckill)->序列化-》bytes[]
try {
Jedis jedis = Resource();
try {
String key = "seckill:" + SeckillId();
// protostuff⼯具
// 将seckill对象序列化成字节数组
byte[] bytes = ByteArray(seckill, schema,
LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
// 缓存时间+key标记+对象序列化结果=》放进缓存jedis缓存池,返回结果result(OK/NO) int timeout = 60 * 60; // 1⼩时
// 设置key对应的字符串value,并给⼀个超期时间
operString result = jedis.Bytes(), timeout, bytes);
mysql是什么系统return result;
} finally {
jedis.close();
}
} catch (Exception e) {java多线程书籍推荐知乎
System.out.Message());
}
return null;
}
}
第三、配置⽂件对象注⼊:
在l⽂件中为序列化缓存类添加注⼊参数,⽤于⾃动实例化对象;第四,编写测试类:
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论