spring boot集成redis和mongodb實現計步排名

源碼url: https://github.com/zhzhair/stepsrank-spring-boot.git。html

1.建立32個分表,用定時任務插入計步數據模擬用戶上傳步數;
2.項目啓動初始化:將32個表的前200名記錄插入mongodb的一個集合(表),清空後插入前200名記錄,
並將第200名的步數(閾值)放到redis;
3.上傳步數時,當用戶的步數大於閾值時,就插入mongodb,不然不插入記錄到mongodb;
4.用定時任務每隔10秒刪除mongodb表中205名之後的記錄;
5.用定時任務每隔1秒更新第200名的步數(閾值)到redis,同時將前200名記錄放進redis的隊列;
6.查詢步數排名先到redis隊列,查不到就去mongodb表查。
7.jmeter併發測試看查詢性能。java

 

程序設計簡述:mysql

技術架構:java8,spring boot2.0.0,mysql,redis,mongodb,mybatis,swagger,jmeter,idea,maven。
  (i)添加測試數據:新建32個表,按照用戶id對32取模添加測試數據到不一樣的表,作定時任務,每秒添加或修改300條記錄。表包括user_id和步數step_count兩個字段,假設手機每隔一段時間傳一次累計步數,若是當日用戶有記錄,就修改用戶的步數(增長新的步數),不然直接添加記錄。部分代碼以下:
@LogForTask
@Scheduled(cron = "0/1 * * * * ?")
public void uploadStep(){//定時任務每秒添加或修改300條記錄
  IntStream.range(0,300).parallel().forEach(i->stepService.uploadStep(32));
}
  (ii)程序設計:在高併發的狀況下內存是個問題(out of memory exception!),單個mongodb文檔也不能放太多的數據,因此須要設置內存不足就讀取磁盤。考慮到第200名的總步數不會減小,而且越日後越「穩定」,因此把它做爲閾值就能夠給查詢的表「瘦身」,從而避免大表排序。
  初始化(即啓動項目時):須要將32個表的前200名都放到一個mongodb文檔,再將文檔前200名替換到該bson文檔,同時將第200名的步數存到redis裏面,部分代碼以下:
@Resource
private StepService stepService;
private static StepService service;
@PostConstruct
public void init(){
  service = this.stepService;
}git

public static void main(String[] args) {
SpringApplication.run(StepsApplication.class, args);
  //啓動項目初始化排名
  service.recordTopAll(32);
}

@Override
public void recordTopAll(int tableCount) {
  mongoTemplate.dropCollection(StepsTop.class);//刪除文檔
  IntStream.range(0,tableCount).parallel().forEach(this::insertOneTable);//將MySQL的數據插入到mongo文檔
  /*取出前200名放到list,更新mongo文檔的數據爲當前list的數據*/
  Query query = new Query().with(new Sort(Sort.Direction.DESC,"totalCount")).limit(200);
  List<StepsTop> list = mongoTemplate.find(query,StepsTop.class);
  if(list.isEmpty()) return;
  mongoTemplate.dropCollection(StepsTop.class);
  mongoTemplate.insertAll(list);
  /*redis保存閾值-第200名的步數*/
  int size = Math.min(200,list.size());
  redisTemplate.opsForValue().set(redisKey,String.valueOf(list.get(size - 1).getTotalCount()));
}
  步數上傳:redis的數據作定時任務更新,閾值愈來愈大,每次都將接收到的步數或更新後的步數與閾值比較,比這個閾值大才會去查mongo,而後對mongo文檔作更新或插入操做,這個「比較」會很是頻繁,可是redis「不害怕」高併發,咱們沒必要擔憂。這樣就大大地減小了對mongo文檔的操做,確保mongo文檔數據量不多,以後查詢並排序mongo文檔的數據就很快了。部分代碼以下:
@Override
public void uploadStep(int tableCount) {
  int userId = new Random().nextInt(500_0000);
  int stepCount = 1 + new Random().nextInt(5000);
  Integer count = commonMapper.getStepCount(prefix + userId%tableCount,userId);
  if(count != null){
    commonMapper.updateSteps(prefix + userId%tableCount, userId,count + stepCount);
  }else{
    commonMapper.insertTables(prefix + userId%tableCount, userId, stepCount);
  }
  String tailSteps = redisTemplate.opsForValue().get(redisKey);
  int totalCount = count == null?stepCount:count + stepCount;
  if(tailSteps != null && totalCount > Integer.valueOf(tailSteps)){//步數超過閾值就插入或更新用戶的記錄
    Query query = new Query(Criteria.where("userId").is(userId));
    if(!mongoTemplate.exists(query,StepsTop.class)){
      StepsTop stepsTop = new StepsTop();
      stepsTop.setUserId(userId);
      stepsTop.setTotalCount(stepCount);
      mongoTemplate.insert(stepsTop);
    }else{
      System.out.println("update: " + tailSteps);
      Update update = new Update();
      update.set("totalStep",totalCount);
      mongoTemplate.upsert(query,update,StepsTop.class);
    }
  }else{
    StepsTop stepsTop = new StepsTop();
    stepsTop.setUserId(userId);
    stepsTop.setTotalCount(stepCount);
    mongoTemplate.insert(stepsTop);
  }
}
  定時任務:每隔10秒更新一次閾值,同時刪除mongo文檔中200名之外的數據;每隔1秒從mongo查詢排好序的前200名的數據push到redis隊列,方便從redis取出排名。部分代碼以下:
@Override//更新閾值,刪除mongo文檔中200名之外的數據
public void flushRankAll() {
  // Query query = new Query().with(new Sort(Sort.Direction.DESC,"totalCount")).limit(201);
  // List<StepsTop> list = mongoTemplate.find(query,StepsTop.class);//高併發場景下容易出現內存不足異常:out of memory Exception
  TypedAggregation<StepsTop> aggregation = Aggregation.newAggregation(
    StepsTop.class,
    project("userId", "totalCount"),//查詢用到的字段
    sort(Sort.Direction.DESC,"totalCount"),
    limit(200)
  ).withOptions(newAggregationOptions().allowDiskUse(true).build());//內存不足到磁盤讀寫,應對高併發
  AggregationResults<StepsTop> results = mongoTemplate.aggregate(aggregation, StepsTop.class, StepsTop.class);
  List<StepsTop> list = results.getMappedResults();
  if(list.size() == 201){
    int totalCount = list.get(199).getTotalCount();
    Query query1 = new Query(Criteria.where("totalCount").lt(totalCount));
    mongoTemplate.remove(query1,StepsTop.class);
  }
}
@Override//查詢排好序的前200名的數據push到redis隊列
public void recordRankAll() {
  // Query query = new Query().with(new Sort(Sort.Direction.DESC,"totalCount")).limit(200);
  // List<StepsTop> list = mongoTemplate.find(query,StepsTop.class);
  TypedAggregation<StepsTop> aggregation = Aggregation.newAggregation(
    StepsTop.class,
    project("userId", "totalCount"),//查詢用到的字段
    sort(Sort.Direction.DESC,"totalCount"),
    limit(200)
  ).withOptions(newAggregationOptions().allowDiskUse(true).build());//內存不足到磁盤讀寫,應對高併發
  AggregationResults<StepsTop> results = mongoTemplate.aggregate(aggregation, StepsTop.class, StepsTop.class);
  List<StepsTop> list = results.getMappedResults();
  if(list.size() == 200){
    Integer stepCount = list.get(199).getTotalCount();
    redisTemplate.opsForValue().set(redisKey,String.valueOf(stepCount));
  }
  if(!list.isEmpty()){
    redisListTemplate.delete(redisQueueKey);
    //noinspection unchecked
    redisListTemplate.opsForList().rightPushAll(redisQueueKey,list);
  }
}
  查詢排行榜:如今就簡單了,直接到redis隊列查詢便可,部分代碼以下:
@ApiOperation(value = "查詢當日總步數排名", notes = "查詢當日總步數排名")
@RequestMapping(value = "/getRankAll", method = {RequestMethod.GET}, produces = {MediaType.APPLICATION_JSON_VALUE})
public BaseResponse<List<StepsRankAllResp>> getRankAll(int begin,int pageSize) {
  BaseResponse<List<StepsRankAllResp>> baseResponse = new BaseResponse<>();
  List<StepsRankAllResp> list = stepService.getRankAllFromRedis(begin,pageSize);
  if(list.isEmpty()) list = stepService.getRankAll(begin,pageSize);//redis查不到數據就從Mongo查
  baseResponse.setCode(0);
  baseResponse.setMsg("返回數據成功");
  baseResponse.setData(list);
  return baseResponse;
}
@Override//todo 從redis讀取
public List<StepsRankAllResp> getRankAllFromRedis(int begin, int pageSize) {
  List<StepsTop> stepsList = redisListTemplate.opsForList().range(redisQueueKey,begin,pageSize);
  List<StepsRankAllResp> list = new ArrayList<>(stepsList.size());
  for (int i = 0; i < stepsList.size(); i++) {
    StepsRankAllResp stepsRankAllResp = new StepsRankAllResp();
    StepsTop stepsTop = stepsList.get(i);
    BeanUtils.copyProperties(stepsTop,stepsRankAllResp);
    stepsRankAllResp.setRank(begin + i + 1);
    list.add(stepsRankAllResp);
  }
  return list;
}
  jmeter併發測試:訪問接口文檔--http://localhost:8080/swagger-ui.html/,調接口查詢排名,配置調接口5000次,持續5秒,聚合報告以下:github

相關文章
相關標籤/搜索