高并发处理,CDN,redis---31

高并发处理,CDN,redis---31

高并发分析

高并发处理,CDN,redis---31_第1张图片

详情页:

CDN的理解:

(1)CDN (内容分发网络)加速用户获取数据的 系统 (2)部署在 离用户最近 网络节点 上 (3) 命中CDN 不需要访问 后端服务器 (4)互联网公司 自己搭建 租用
高并发处理,CDN,redis---31_第2张图片

秒杀地址接口优化:

1.无法使用CDN缓存
2.适合服务端缓存:redis等
3.一致性维护成本低
 
  

秒杀操作优化分析:

1.无法使用CDN缓存
2.后端缓存困难:库存问题
3.一行数据竞争:热门商品

Mysql压力测试(4wQPS)

高并发处理,CDN,redis---31_第3张图片


Java控制事务行为分析

高并发处理,CDN,redis---31_第4张图片

瓶颈分析--java客户端执行sql事务,update有行级锁(有网络延迟和GC)影响速度

优化方向--减少行级锁持有时间

延迟分析

高并发处理,CDN,redis---31_第5张图片

优化思路:把客户端逻辑放到MySQL服务器端,避免网络延时和GC影响;

放到MySQL服务端 两种方案:


1.定制SQL方案:update/* +[auto_commit]*/,需要修改MySQL源码

2.使用存储过程:整个事务在MySQL端完成

优化总结:

高并发处理,CDN,redis---31_第6张图片

redis后端缓存优化:地址暴露接口(超时维护:不变化的数据)

redis网址:www.redis.io

windows版:redis网址 https://github.com/MSOpenTech/redis/releases

用jedis Java客户端操作java对象缓存的api方法,和谷歌的protostuff序列化开源方案(比Java原生的序列化效率要高)来序列化和反序列化对象,缓存对象必须转换为byte字节数组存入redis缓存,不像memcached可以直接缓存对象
实际操作思路是初次查询redis缓存,如果没有就从数据库取出对象放入redis缓存,下次取就会直接从redis缓存取数据了。但缓存有超时时间限制

@pom.xml增加依赖

		
		
			redis.clients
			jedis
			2.7.3
				
		
		
			com.dyuproject.protostuff
			protostuff-core
			1.0.8
				
		   
			com.dyuproject.protostuff
			protostuff-runtime
			1.0.8
		

@RedisDao.java缓存操作文件

public class RedisDao {
	private final Logger logger = LoggerFactory.getLogger(this.getClass());
	private final JedisPool jedisPool;
	private RuntimeSchema schema = RuntimeSchema.createFrom(Seckill.class);
	
	public RedisDao (String ip,int port){
		jedisPool = new JedisPool(ip, port);
	}
	
	public Seckill getSeckill(long seckillId){
		//redis操作逻辑
		try {
			Jedis jedis = jedisPool.getResource();
			try {
				String key = "seckill:"+seckillId;
				//并没有实现内部序列化操作
				//get ->byte[] -> 反序列化 ->Object(Seckill)
				//采用自定义序列化--开源社区protostiff
				//protostiff:pojo.
				byte[] bytes=jedis.get(key.getBytes());
				//缓存获取到
				if(bytes != null){
					//空对象--待反序列赋值
					Seckill seckill = schema.newMessage();
					//反序列操作--将bytes根据schema转化为seckill对象
					//比原生快2个数量级,空间是1/10~1/5
					ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
					return seckill;
				}
				
			} finally {
				jedis.close();
			}
		} catch (Exception e) {
			logger.error(e.getMessage(),e);
		}
		return null;
	}
	public String putSeckill(Seckill seckill){
		//set Object(Seckill)->序列化->byte[]
		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) {
			logger.error(e.getMessage(),e);
		}
		return null;
	}
	
}

@spring-dao.xml 注入编写(构造器注入)

 	
 	 
 	 	
 	 	
 	 	
 	 

@RedisDaoTest.java编写测试类

@RunWith(SpringJUnit4ClassRunner.class)
//告诉Junit spring配置文件
@ContextConfiguration(locations={"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {

	private long seckillId = 1001;
	@Autowired
	private RedisDao redisDao;
	@Autowired
	private SeckillDao seckillDao;
	
	@Test//get and put
	public void testGetSeckill() {
		Seckill seckill = redisDao.getSeckill(seckillId);
		if (seckill == null) {
			seckill = seckillDao.queryById(seckillId);
			if(seckill != null){
				String result = redisDao.putSeckill(seckill);
				System.out.println("result"+result);
				seckill = redisDao.getSeckill(seckillId);
				System.out.println("seckill="+seckill);
			}
		}	
	}
}

@SeckilServiceImpl.java Service实现层改写(增加Radis缓存--减少访问数据库)

	public Exposer exportSeckillUrl(long seckillId) {
		//优化点:缓存优化:超时的基础上维护一致性(数据不变的情况,多并发)
		//1:访问redis
		Seckill seckill = redisDao.getSeckill(seckillId);
		if (seckill == null) {		
			//2:访问数据库
			seckill = seckillDao.queryById(seckillId);
			if(seckill != null){
				//3:放入redis
				String result = redisDao.putSeckill(seckill);
				System.out.println("result"+result);
				seckill = redisDao.getSeckill(seckillId);
				System.out.println("seckill="+seckill);
			}else{
				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, seckillId, md5);
	}

Mysql事务优化:

瓶颈主要出现在网络延时和GC操作

简单优化:将update后移,减少rowLock时间。

高并发处理,CDN,redis---31_第7张图片

@SeckillServiceImpl.java 秒杀事务优化

	/**
	 * 使用注解控制事务方法的优点:
	 * 1.开发团队达成一致约定,明确标注事务方法的编程风格。
	 *2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
	 *3.不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制
	 */
	@Transactional
	public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
			throws SeckillException, RepeatKillException, SeckillCloseException {
		System.out.println("执行executeSeckill。。。");
		if (md5 == null || !md5.equals(getMD5(seckillId))) {
			throw new SeckillException("seckill data rewrite");
		}
		Date killTime = new Date();
		// 执行秒杀逻辑:减库存+记录秒杀行为(一个事务(运行期),出现问题,回滚)
		try {
			// 记录购买行为
			int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
			// 唯一验证:seckillId,userPhone
			// 重复秒杀
			if (insertCount <= 0){
				throw new RepeatKillException("seckill repeated");
			} else {
				// 减库存 ,热门商品竞争
				System.out.println("减库存。。。");
				int updateCount = seckillDao.reduceNumber(seckillId, killTime);
				System.out.println("减库存完成");
				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);
				}
			}
		} 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());
		}
	}

}

深度优化:事务SQL在MySQL端执行(存储过程--使insert和update在Mysql端执行)

@seckill.sql存储过程源码

 
   
-- 秒杀执行存储过程
DELIMITER $$ -- 表示命令界定符设置为了$$ console ; 转换为 $$
-- 定义存储过程
-- 参数:in 输入参数; out 输出参数
-- row_count():返回上一条修改类型sql(delete,insert,update)的影响行数
-- row_count: 0:未修改数据; >0:表示修改的行数; <0:sql错误/未执行修改sql
CREATE PROCEDURE `seckill`.`execute_seckill`
(IN v_seckill_id bigint, IN v_phone bigint,
IN v_kill_time TIMESTAMP, OUT r_result int)
	BEGIN
		DECLARE insert_count INT DEFAULT 0;
		START TRANSACTION;
		INSERT ignore INTO success_killed (seckill_id, user_phone, create_time)
		VALUES(v_seckill_id, v_phone, v_kill_time);
		SELECT ROW_COUNT() INTO insert_count;
		IF (insert_count = 0) THEN
			ROLLBACK;
			SET r_result = -1;
		ELSEIF (insert_count < 0) THEN
			ROLLBACK ;
			SET r_result = -2;
		ELSE
			UPDATE seckill SET number = number - 1
			WHERE seckill_id = v_seckill_id AND end_time > v_kill_time
			AND start_time < v_kill_time AND number > 0;
			SELECT ROW_COUNT() INTO insert_count;
			IF (insert_count = 0) THEN
				ROLLBACK;
				SET r_result = 0;
			ELSEIF (insert_count < 0) THEN
				ROLLBACK;
				SET r_result = -2; 
			ELSE
				COMMIT;
			SET r_result = 1;
			END IF;
		END IF;
	END;
$$
-- 代表存储过程定义结束

DELIMITER ;
-- 定义一个变量
SET @r_result = -3;
-- 执行存储过程
call execute_seckill(1001, 13631231234, now(), @r_result);
-- 获取结果
SELECT @r_result;

-- 存储过程
-- 1.存储过程优化:事务行级锁持有的时间
-- 2.不要过度依赖存储过程
-- 3.简单的逻辑可以应用存储过程
-- 4.QPS:一个秒杀单6000/qps

@SeckillService.java接口增加存储过程方法

	/**
	 * 执行秒杀操作by存储过程
	 * @param seckillId
	 * @param userPhone
	 * @param md5
	 * @return SeckillExecution
	 */
	SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5)
		throws SeckillException,RepeatKillException,SeckillCloseException;

@SeckillDao.java接口增加存储过程方法

		/**
		 * 使用存储过程执行秒杀
		 * @param paramMap
		 */
		void killByprocedure(Map paramMap);

@SeckillDao.xml实现MyBatis调用存储过程