高并发优化分析
-
高并发发生在哪里
红色区域都是高并发发生的点
-
为什么要单独获取系统时间
因为detail页和他的静态化资源都放在CDN上,所以访问的时候就不再请求我们的服务器,也就不能拿到系统时间,所以要有一个接口来返回系统时间
什么是CDN
-
获取秒杀地址接口分析
这个没法使用CDN,因为CDN对应的资源是不宜变化的,而获取秒杀接口的返回里面的数据(开始时间,结束时间)在变化
他适合用redis等服务的缓存
- 秒杀操作优化分析
① 无法使用CDN
② 后端缓存困难:库存问题,要使用Mysql保证事务
③ 一行数据竞争:热点商品操作
为什么不用Mysql处理
因为Mysql低效,这里的低效的原因是java客户端执行这些操作时候和数据库的通信有网络延迟和GC,而update这些操作不同用户线程又是串行执行的,会等待
行级锁是在commit或rollback才释放,我们优化就是要减少行级锁持有时间,也就是说可以把客户端逻辑放在MySql服务器端,避免网络延迟和GC影响
并发优化
这里可以改变insert和update的顺序,达到行级锁持有时间变短的目的(insert 和 update两个依旧组成事务,如果update不成功会回滚)
@Override
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
if(md5 == null || !md5.equals(getMD5(seckillId))){
throw new SeckillException("seckill data rewrite");
}
//执行秒杀逻辑:减库存+记录购买记录
Date nowTime = new Date();
try {
int insertCount = successKilledDao.insertSuccessKilled(seckillId,userPhone);
//userPhone和seckillId作为联合主键避免重复秒杀
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, SeckillStaEnum.SUCCESS,successKilled);
}
}
}catch (SeckillCloseException e1){
throw e1;
} catch (RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new SeckillException("seckill inner error:"+e.getMessage());
}
}
redis后端缓存优化
因为要频繁的访问后端获取秒杀暴露接口,那么可以把他放到redis缓存起来
- 引入java访问redis的客户端
<!-- redis客户端:jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.7.3</version>
</dependency>
-
缓存控制不应该放在service层,而应该放在dao层,dao数据访问对象就是用来放数据库存储和其他数据访问的包
-
我们想① 通过redis访问一个缓存的对象,② 缓存里没有的时候往里放一个
-
我们要实现序列化,因为redis并没有实现内部序列化操作,从redis里面读出来的二进制数组,我们要通过反序列化来转化为对象
对于序列化来说,直接implements Serializable,但是这是高并发优化,使用protostuff更高效,使用自定义序列化 -
引入对应依赖
<!--prostuff序列化依赖-->
<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>
-
取是从redis中取出字节数组,反序列化为java对象
-
放是把java对象序列化为byte[]数组,放入
package org.seckill.dao.cache;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import org.seckill.entity.Seckill;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import java.util.concurrent.ExecutionException;
public class RedisDao {
private final JedisPool jedisPool;
public RedisDao(String ip, int port){
jedisPool = new JedisPool(ip,port);
}
private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
public Seckill getSeckill(long seckillId){
try{
Jedis jedis = jedisPool.getResource();
try{
String key = "sckill:"+seckillId;
//并没有实现哪部序列化操作
//采用自定义序列化
//protostuff: pojo.
byte[] bytes =jedis.get(key.getBytes());
//获取到
if(bytes!=null){
//反序列化
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes,seckill,schema);
return seckill;
}
}finally {
jedis.close();
}
}catch (Exception e){
}
return null;
}
public String putSeckill(Seckill seckill){
try{
Jedis jedis = jedisPool.getResource();
try{
String key = "seckill:"+seckill.getSeckillId();
byte[] bytes = ProtostuffIOUtil.toByteArray(seckill,schema, LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
//超时缓存
int timeout = 60*60;//1小时
String result = jedis.setex(key.getBytes(),timeout,bytes);
return result;
}finally {
jedis.close();
}
}catch(Exception e){
}
return null;
}
}
- 注入
<!--redisDao-->
<bean id="redisDao" class="org.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="localhost"/>
<constructor-arg index="1" value="6379"/>
</bean>
- service中使用
@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.putSeckill(seckill);
}
}
if(seckill == null){//差不到秒杀记录
return new Exposer(false,seckillId);
}
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(),startTime.getTime(), endTime.getTime());
}
//秒杀开始
String md5 = getMD5(seckillId);//加密,转化特定的字符串,不可逆
return new Exposer(true,md5,seckillId);
}
事务SQL在MySql端执行
存储过程优化事务行级锁的持有时间