(四)Java高併發秒殺API之高併發優化

Java團長_發表於2018-12-12

下載Redis

  • 下載完後解壓壓縮包

  • 進入解壓後的資料夾裡面 ,執行命令 make

  • 然後再執行sudo make install

  • 最後再啟動REdis,啟動命令為redis-server

  • 執行命令'redis-cli -p 6379'檢視執行情況

使用Java操作Redis

  • 匯入操作Redisjedis的 jar包

<dependency>
           <groupId>redis.clients</groupId>
           <artifactId>jedis</artifactId>
           <version>2.9.0</version>
       </dependency>


  • 新增protostuff-core以及protostuff-runtime序列化jar包


<dependency>
           <groupId>com.dyuproject.protostuff</groupId>
           <artifactId>protostuff-core</artifactId>
           <version>1.1.1</version>
       </dependency>
       <dependency>
           <groupId>com.dyuproject.protostuff</groupId>
           <artifactId>protostuff-runtime</artifactId>
           <version>1.1.1</version>
       </dependency>


com.suny.dao下建包cache

  • 然後建立類RedisDao


/**
* 操作Redis的dao類
* Created by 孫
*/

public class RedisDao {
  private final Logger logger = LoggerFactory.getLogger(this.getClass());

  private final JedisPool jedisPool;

  private RuntimeSchema<Seckill> 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()) {
          String key = "seckill:" + seckillId;
          // 並沒有實現內部序列化操作
          //get->byte[]位元組陣列->反序列化>Object(Seckill)
          // 採用自定義的方式序列化
          // 快取獲取到
          byte[] bytes = jedis.get(key.getBytes());
          if (bytes != null) {
              // 空物件
              Seckill seckill = schema.newMessage();
              ProtostuffIOUtil.mergeFrom(bytes, seckill, schema);
              // seckill被反序列化
              return seckill;
          }
      } catch (Exception e) {
          logger.error(e.getMessage(), e);
      }
      return null;
  }

  public String putSeckill(Seckill seckill) {
      //  set Object(Seckill) -> 序列化 -> byte[]
      try (Jedis jedis = jedisPool.getResource()) {
          String key = "seckill:" + seckill.getSeckillId();
          byte[] bytes = ProtostuffIOUtil.toByteArray(seckill, schema,
                  LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));
          // 超時快取
          int timeout=60*60;
          return jedis.setex(key.getBytes(), timeout, bytes);
      } catch (Exception e) {
          logger.error(e.getMessage(), e);
      }
      return null;
  }
}


  • 下一步是在在applicationContext-dao.xml中注入redisDao


<!--注入redisDao-->
   <bean id="redisDao" class="com.suny.dao.cache.RedisDao">
       <!--構造方法注入值-->
       <constructor-arg index="0" value="localhost"/>
       <constructor-arg index="1" value="6379"/>
   </bean>


  • 改造exportSeckillUrl方法,一定要先注入redisDao


@Autowired
   private RedisDao redisDao;


@Override
   public Exposer exportSeckillUrl(long seckillId) {
       // 根據秒殺的ID去查詢是否存在這個商品
      /* Seckill seckill = seckillMapper.queryById(seckillId);
       if (seckill == null) {
           logger.warn("查詢不到這個秒殺產品的記錄");
           return new Exposer(false, seckillId);
       }*/

       Seckill seckill = redisDao.getSeckill(seckillId);
       if (seckill == null) {
           // 訪問資料庫讀取資料
           seckill = seckillMapper.queryById(seckillId);
           if (seckill == null) {
               return new Exposer(false, seckillId);
           } else {
               // 放入redis
               redisDao.putSeckill(seckill);
           }
       }

       // 判斷是否還沒到秒殺時間或者是過了秒殺時間
       LocalDateTime startTime = seckill.getStartTime();
       LocalDateTime endTime = seckill.getEndTime();
       LocalDateTime nowTime = LocalDateTime.now();
       //   開始時間大於現在的時候說明沒有開始秒殺活動    秒殺活動結束時間小於現在的時間說明秒殺已經結束了
       if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) {
           //秒殺開啟,返回秒殺商品的id,用給介面加密的md5
           String md5 = getMd5(seckillId);
           return new Exposer(true, md5, seckillId);
       }
       return new Exposer(false, seckillId, nowTime, startTime, endTime);


   }


  • 寫儲存過程,然後去Mysql控制檯執行儲存過程


-- 秒殺執行儲存過程
DELIMITER $$ -- console ; 轉換為
$$
-- 定義儲存過程
-- 引數: in 引數   out輸出引數
-- row_count() 返回上一條修改型別sql(delete,insert,update)的影響行數
-- row_count:0:未修改資料 ; >0:表示修改的行數; <0: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(1003, 13502178891, now(), @r_result);
-- 獲取結果
SELECT @r_result;


  • SeckillMapper中編寫killProduce()方法


/**
    *  使用儲存過程執行秒殺
    * @param paramMap
    */

   void killByProcedure(Map<String,Object> paramMap);


  • 然後在SeckillMapper.xml中寫sql語句


<!--呼叫儲存過程-->
   <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>


  • 下一步在SeckillService介面中中編寫 killProduce()方法


SeckillExecution executeSeckillProcedure(long seckillId,long userPhone,String md5);


  • 匯入commons-collections工具類


<!--匯入apache工具類-->
       <dependency>
           <groupId>commons-collections</groupId>
           <artifactId>commons-collections</artifactId>
           <version>3.2.2</version>
       </dependency>


  • 然後SeckillServiceImpl實現killProduce()方法


@Override
   public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5)
{
       if (md5 == null || !md5.equals(getMd5(seckillId))) {
           return new SeckillExecution(seckillId, SeckillStatEnum.DATE_REWRITE);
       }
       LocalDateTime killTime = LocalDateTime.now();
       Map<String, Object> map = new HashMap<>();
       map.put("seckillId", seckillId);
       map.put("phone", userPhone);
       map.put("killTime", killTime);
       map.put("result", null);
       // 執行儲存過程,result被複制
       try {
           seckillMapper.killByProcedure(map);
           // 獲取result
           int result = MapUtils.getInteger(map, "result", -2);
           if (result == 1) {
               SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
               return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
           } else {
               return new SeckillExecution(seckillId, SeckillStatEnum.stateOf(result));
           }
       } catch (Exception e) {
           logger.error(e.getMessage(), e);
           return new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
       }
   }


  • 改造執行秒殺executeSeckill方法,減少一道虛擬機器GC程式,優化效能


@Transactional
   @Override
   public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException {
       if (md5 == null || !md5.equals(getMd5(seckillId))) {
           logger.error("秒殺資料被篡改");
           throw new SeckillException("seckill data rewrite");
       }
       // 執行秒殺業務邏輯
       LocalDateTime nowTIme = LocalDateTime.now();

       try {
           // 記錄購買行為
           int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone);
           if (insertCount <= 0) {
               // 重複秒殺
               throw new RepeatKillException("seckill repeated");
           } else {
               // 減庫存 ,熱點商品的競爭
               int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme);
               if (reduceNumber <= 0) {
                   logger.warn("沒有更新資料庫記錄,說明秒殺結束");
                   throw new SeckillCloseException("seckill is closed");
               } else {
                   // 秒殺成功了,返回那條插入成功秒殺的資訊  進行commit
                   SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone);
                   return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
               }
           }
       } catch (SeckillCloseException | RepeatKillException e1) {
           throw e1;
       }

   }


  • 編寫SeckillServiceImpl中的killProduce()方法的測試方法


@Test
   public void executeSeckillProcedureTest()
{
       long seckillId = 1001;
       long phone = 1368011101;
       Exposer exposer = seckillService.exportSeckillUrl(seckillId);
       if (exposer.isExposed()) {
           String md5 = exposer.getMd5();
           SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5);
           System.out.println(execution.getStateInfo());
       }
   }


  • 改造SeckillController中的execute方法呼叫,把一開始呼叫普通方法的改成呼叫儲存過程的那個方法


@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST)
    @ResponseBody
    public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") long seckillId,
                                                   @PathVariable("md5") String md5,
                                                   @CookieValue(value = "userPhone", required = false) Long userPhone) {
        // 如果使用者的手機號碼為空的說明沒有填寫手機號碼進行秒殺
        if (userPhone == null) {
            return new SeckillResult<>(false, "沒有註冊");
        }
        // 根據使用者的手機號碼,``秒殺商品的id跟md5進行秒殺商品,沒異常就是秒殺成功
        try {
            // 這裡換成儲存過程

//            SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
            SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userPhone, md5);
            return new SeckillResult<>(true, execution);
        } catch (RepeatKillException e1) {
            // 重複秒殺
            SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
            return new SeckillResult<>(false, execution);
        } catch (SeckillCloseException e2) {
            // 秒殺關閉
            SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
            return new SeckillResult<>(false, execution);
        } catch (SeckillException e) {
            // 不能判斷的異常
            SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
            return new SeckillResult<>(false, execution);
        }
        // 如果有異常就是秒殺失敗
    }


程式碼


https://github.com/Sunybyjava/seckill


(完)

相關文章