Java高併發秒殺API之高併發優化(四)
四 高併發優化
1.分析
1.詳情頁 部署到cdn上,這樣使用者訪問的是cdn不是伺服器了。
使用者在上網時通過運營商訪問最近的都會網路,都會網路訪問主幹網。
2.獲取系統時間 不用優化
訪問一次記憶體大概 10ns
無法使用cdn,適合伺服器端快取redis等(單臺一秒10萬qps,還可以做叢集)
一致性維護非常低
3.秒殺地址優化
請求地址->訪問redis–(超時穿透/主動重新整理)->訪問mysql
4.秒殺操作的優化分析
無法使用cdn
後端快取困難,庫存問題
一行資料競爭,熱點商品
其他方案分析
運維成本高(nosql不穩定等),開發成本高(開發需要知道事務回滾等)
冪等性難保證,重複秒殺
mysql update同一條資料 ,4萬qps
A :先update 後insert
B 先update 等待事務 ,A釋放鎖後update,insert
gc(新生代gc(暫停所有java程式碼,幾十毫秒 )和老一代gc)
執行update –網路延遲/gc –>insert–網路延遲/gc –>commit/rollback
優化方向減少鎖持有時間
同城機房(0.5~2ms)max(1000qps)
異地機房更長
如何判斷update成功
1.自身沒有報錯2.客戶端確認更新成功
優化思路
把客戶端的業務邏輯放到mysql服務端
兩種解決方案
1.定製sql方案update /*+[auto_commit]*/
自動進行update為1 -> commit ,為0 -> rollback
2.儲存過程
2.redis 後端優化
官網下載redis
https://redis.io/download
安裝完成後
redis-server伺服器啟動
redis-cli 客戶端啟動
引入jedis依賴
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version >2.7.3</version>
</dependency>
redis指令 get key ,set key value
redisDao
package org.seckill.dao.cache;
import org.seckill.entity.Seckill;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.dyuproject.protostuff.LinkedBuffer;
import com.dyuproject.protostuff.ProtostuffIOUtil;
import com.dyuproject.protostuff.runtime.RuntimeSchema;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
public class RedisDao {
private final JedisPool jedisPool;
private final Logger logger = LoggerFactory.getLogger(RedisDao.class);
private RuntimeSchema<Seckill> schema = RuntimeSchema.createFrom(Seckill.class);
public RedisDao(String ip,int port){
jedisPool = new JedisPool(ip,port);
}
public Seckill getSeckill(long seckillId){
try {
Jedis jedis = jedisPool.getResource();
try {
String key = "seckill:"+seckillId;
//沒有實現 內部序列化
//get -> byte[] -> 反序列化 ->Object(seckill)
//採用自定義序列化
// protostuff:pojo(有get,set方法)
byte [] bytes = jedis.get(key.getBytes());
if(bytes != null){
//建立一個空物件
Seckill seckill = schema.newMessage();
ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
//被反序列
return seckill;//比java 原生的壓縮了1/10~1/5 壓縮速度 差2個數量級
}
}finally{
jedis.close();
}
} catch (Exception e) {
logger.error(e.getMessage());
}
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;//小時
String result = jedis.setex(key.getBytes(), timeout, bytes);
return result;
} finally{
jedis.close();
}
} catch (Exception e) {
logger.error(e.getMessage());
}
return null;
}
}
key存放seckill:id value存放序列化物件
所以需要protostuff(效能更好)serializable(效能較低)
新增依賴
<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 -->
<!-- 構造方法注入 -->
<bean id ="redisDao" class = "org.seckill.dao.cache.RedisDao">
<constructor-arg index="0" value="localhost"> </constructor-arg>
<constructor-arg index="1" value="6379"> </constructor-arg>
</bean>
單元測試
RedisDaoTest
package org.seckill.dao;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dao.cache.RedisDao;
import org.seckill.entity.Seckill;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class RedisDaoTest {
@Autowired
private RedisDao redisDao;
private long id = 1002;
@Autowired
private SeckillDao seckillDao;
@Test
public void testSeckill() throws Exception{
//get and put
Seckill seckill = redisDao.getSeckill(id);
if(seckill == null){
seckill = seckillDao.queryById(id);
if(seckill != null){
String result = redisDao.putSeckill(seckill);
System.out.println(result);
seckill = redisDao.getSeckill(id);
System.out.println(seckill);
}
}
}
@Test
public void testGetSeckill() throws Exception{
Seckill seckill = redisDao.getSeckill(id);
System.out.println(seckill);
}
@Test
public void testputSeckill() throws Exception{
}
}
3.併發優化
原來的邏輯 update->insert->commit/rollback
修改為insert->update->commit/rollback
減少鎖持有的時間
儲存過程
--- 秒殺執行的儲存過程
--;
DELIMITER $$ --console ; 轉換為 $$
CREATE PROCEDURE `SECKILL`.`execute_seckill`
--引數 in 輸入引數 out 輸出引數
-- row_count():返回上一條修改型別sql(d,i,u)的影響行數
--row-count() = 0 未修改資料 >0 表示修改的行數 <0 sql錯誤/未執行
(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,state,create_time)
values(v_seckill_id,v_phone,0,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,13934131331,now(),@r_result)
select @r_result
SeckillDao新增方法
/**
* 使用儲存過程執行秒殺
* @param parammap
*/
void killByProcedure(Map<String,Object> paramMap);
SeckillDao.xml新增
<select id="killByProcedure" statementType="CALLABLE">
call execute_seckill(
#{seckillId,jdbcType=BIGINT,mode=IN},
#{phone,jdbcType=BIGINT,mode=IN},
#{killTime,jdbcType=TIMESTAMP,mode=IN},
#{result,jdbcType=INTEGER,mode=OUT}
)
</select>
service新增
//執行秒殺操作by儲存過程
SeckillExecution excuteSeckillProcedure(long seckillId,long userPhone,String md5) throws RepeatKillException,seckillCloseException,SeckillException;
實現類
public SeckillExecution excuteSeckillProcedure(long seckillId, long userPhone, String md5)
throws RepeatKillException, seckillCloseException, SeckillException {
if(md5==null||!md5.equals(getMD5(seckillId))){
return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
}
Date killTime = new Date();
Map <String, Object>map =new HashMap<String, Object>();
map.put("seckillId", seckillId);
map.put("phone", userPhone);
map.put("killTime", killTime);
map.put("result", null);
try {
seckilldao.killByProcedure(map);
int result = MapUtils.getInteger(map, "result",-2);
if(result==1){
SuccessKilled sk = successkilleddao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,sk);
}else{
return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
}
} catch (Exception e) {
logger.error(e.getMessage());
return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
}
}
系統用到哪些服務
cdn
webserver : nginx(叢集化,http伺服器,給後端伺服器做反向代理)+jetty
redis伺服器端快取
mysql mysql事務,保證資料的一致性和完整性