- 高并发优化分析
并发性能无法提升的原因是当多个线程同时访问一行数据时,会产生事务,从而导致写锁的产生。只有当一个获取事务的线程释放锁后,排队的下一个线程才能获得写锁。qps(每秒查询率)和事务执行时间密切相关,事务执行时间越短,并发性能就越高。这也是将耗时的i/o操作移出事务之外的原因。
高并发发生的地方
在上图中,红色部分表示会发生高并发的地方,绿色部分表示对高并发没有影响。
这为我们秒杀系统的优化奠定了基础。例如,在秒杀开始前,用户频繁刷新秒杀商品详情页面是常见的情况,此时秒杀尚未开始,大量请求发送到服务器会造成不必要的负担。
我们将详情页放置在CDN中,这样用户在访问该页面时就不需要访问我们的服务器,从而起到降低服务器压力的作用。CDN中存储的是静态化的详情页和一些静态资源(如css、JavaScript等),这样我们就无法通过CDN获取系统时间来控制秒杀时段,因此需要单独设计一个请求来获取服务器的系统时间。
立即学习“Java免费学习笔记(深入)”;
详情页
CDN
因为Java访问一次内存(Cacheline)大约需要10ns,1s=10亿ns,也就是说如果不考虑GC,这个操作1s可以执行1亿次。
无法使用CDN缓存,因为CDN适合缓存不变的资源,如静态资源和JavaScript;秒杀地址返回的数据是变化的,不适合放在CDN缓存中;适合使用服务端缓存,如redis等,1秒钟可以承受10万QPS。多个redis组成集群,可以达到100万QPS。因此,后端缓存可以由业务系统控制。
秒杀地址接口优化
无法使用CDN缓存,后端缓存也存在困难:库存问题导致一行数据竞争;热门商品的大部分写操作和核心操作无法使用CDN,也不可能在缓存中减库存。如果在Redis中减库存,用户也可能通过缓存来减库存,这样会导致库存不一致,因此必须通过mysql的事务来保证一致性。
例如,一个热门商品所有人都在抢购,会在同一时间对数据表中的一行数据进行大量的UPDATE SET操作。
行级锁在事务提交后才释放,因此优化方向是减少行级锁的持有时间。
同城机房网络(0.5ms~2ms)的最高并发性是1000QPS。更新后jvm的垃圾回收机制大约需要50ms,最高并发性是20QPS。并发性越高,GC就越可能发生,虽然不一定每次都会发生,但一定会发生。异地机房的网络延迟,如北京到上海之间,经计算大约为13~20ms。
网络延迟计算
有两个条件:
更新操作本身没有报错;客户端确认更新影响的记录数。优化思路:
将客户端逻辑放到MySQL服务端,以避免网络延迟和GC的影响。有两种方案:
定制SQL方案,在每次更新后都会自动提交,但需要修改MySQL源码,成本很高,不是大公司(如BAT等)通常不会使用这种方法。使用存储过程:整个事务在MySQL端完成,用存储过程编写业务逻辑,服务端负责调用。接下来先分析第一种方案。
秒杀方案1
秒杀方案1成本分析
根据上图的成本分析,我们的秒杀系统采用第二种方案,即使用存储过程。
前端控制暴露接口,按钮防重复(点击一次按钮后就变成灰色,禁止重复点击按钮)
动静态数据分离,CDN缓存,后端缓存
事务竞争优化,减少事务行级锁的持有时间
- Redis后端缓存优化编码
由于不同公司提供的CDN接口暴露不同,不同公司租用的机房调用的API也不相同,因此慕课网的视频中并没有对CDN的使用过程进行讲解。
2.1 下载安装Redis
前往官网下载安装Stable版本的Redis,安装后可以将安装目录添加到系统变量Path中以方便使用。我使用的是windows系统的Redis,懒得去官网下载的可以点击这里下载。
安装后,运行redis-server.exe启动服务器成功,接着运行redis-cli.exe启动客户端连接服务器成功,说明Redis已经安装成功了。
Redis属于nosql,即非关系型数据库,它是key-value型数据库,直接在内存中进行存取数据,因此具有很高的性能。
利用Redis可以减轻MySQL服务器的压力,减少与数据库服务器的通信次数。秒杀的瓶颈就在于与数据库服务器的通信速度(MySQL本身的主键查询非常快)。
2.2 在pom.xml中配置Redis客户端
<dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.3</version> </dependency>
Redis有很多客户端,我们的项目是用Java语言编写的,自然选择对应Java语言的客户端,而官网最推荐的Java客户端是Jedis,在pom.xml中配置了Jedis依赖就可以使用它了,记得要先开启Redis的服务器,Jedis才能连接到服务器。
由于Jedis并没有实现内部序列化操作,而Java内置的序列化机制性能又不高,我们是一个秒杀系统,需要考虑高并发优化,在这里我们采用开源社区提供的更高性能的自定义序列化工具protostuff。
2.3 在pom.xml中配置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>
序列化是处理对象流的机制,就是将对象的内容进行流化,可以对流化后的对象进行读写操作,也可以将流化后的对象在网络间传输。反序列化就是将流化后的对象重新转化成原来的对象。
在Java中内置了序列化机制,通过implements Serializable来标识一个对象实现了序列化接口,不过其性能并不高。
2.4 使用Redis优化地址暴露接口
原本查询秒杀商品时是通过主键直接去数据库查询的,选择将数据缓存在Redis中,在查询秒杀商品时先去Redis缓存中查询,以此降低数据库的压力。如果在缓存中查询不到数据再去数据库中查询,再将查询到的数据放入Redis缓存中,这样下次就可以直接从缓存中查询到。