redis使用Zset实现实时排队

实现功能: 入队,出队,实时排队情况,置空队列,分页查询

实现思路: 使用Zset有序集合配合List实现排队功能,Zset的score值即为排队号码,list中根据Zset的排队顺序,存放具体的排队信息

依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId> 
</dependency>

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

实体类

/**
 * @author YudaBao
 * @date 2021/2/25 9:00
 * 返回给用户的排队信息
 */

public class QueryUser {

    private String queryNo;  // 排队号码
    private String queryIndex;  // 当前位置
    private String totalNum;  // 总排队人数
    private String beforeNum;  // 排在前面的人数

    public String getQueryNo() {
        return queryNo;
    }

    public void setQueryNo(String queryNo) {
        this.queryNo = queryNo;
    }

    public String getQueryIndex() {
        return queryIndex;
    }

    public void setQueryIndex(String queryIndex) {
        this.queryIndex = queryIndex;
    }

    public String getTotalNum() {
        return totalNum;
    }

    public void setTotalNum(String totalNum) {
        this.totalNum = totalNum;
    }

    public String getBeforeNum() {
        return beforeNum;
    }

    public void setBeforeNum(String beforeNum) {
        this.beforeNum = beforeNum;
    }
}
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author YudaBao
 * @date 2021/2/25 11:12
 * 存入 list 中的详细排队信息
 */

public class QueryDetail {

    private String queryName;  // 队列名
    private String queryNo;  // 排队号码
    private String queryTime;  // 开始排队时间
    private String queryUserId;  // 排队用户ID

    public QueryDetail() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        this.queryTime = sdf.format(new Date());
    }

    public String getQueryName() {
        return queryName;
    }

    public void setQueryName(String queryName) {
        this.queryName = queryName;
    }

    public String getQueryNo() {
        return queryNo;
    }

    public void setQueryNo(String queryNo) {
        this.queryNo = queryNo;
    }

    public String getQueryTime() {
        return queryTime;
    }

    public void setQueryTime(String queryTime) {
        this.queryTime = queryTime;
    }

    public String getQueryUserId() {
        return queryUserId;
    }

    public void setQueryUserId(String queryUserId) {
        this.queryUserId = queryUserId;
    }
}

service层代码

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

/**
 * @author YudaBao
 * @date 2021/2/28 16:14
 *
 * 使用redis实现的商家排队功能
 */

@Service
public class RedisQueryService {


    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据StoreId获取该店排队的Zset名
     * @param storeId
     * @return
     */
    public String getQueryZsetName(String storeId) {
        return "query" + "-" + "zset" + "-" + storeId;
    }

    /**
     * 根据StoreId获取排队的list名
     * @param storeId
     * @return
     */
    public String getQueryListName(String storeId) {
        return "query" + "-" + "list" + "-" + storeId;
    }


    /**
     * 判断Zset集合中是否有某个元素
     * @return
     */
    public boolean isExistZset(String zsetName,String value) {
        boolean result = false;
        if(redisTemplate.opsForZSet().score(zsetName,value) != null)
            result = true;
        return result;
    }


    /**
     * 获取当前队列的最大score
     * @param storeId
     * @return
     */
    public double getMaxScore(String storeId) {
        //返回1个set,只有1个元素
        double maxScore = 0.0;
        Set<ZSetOperations.TypedTuple<String>> set = redisTemplate.opsForZSet().reverseRangeWithScores(getQueryZsetName(storeId), 0, 0);
        Iterator<ZSetOperations.TypedTuple<String>> iterator = set.iterator();
        while (iterator.hasNext()) {
            ZSetOperations.TypedTuple<String> typedTuple = iterator.next();
            maxScore = typedTuple.getScore();
        }
        return maxScore;
    }

    /**
     * 获取队列当前排队总人数
     * @param storeId
     * @return
     */
    public long curQueryNumber(String queryZsetName) {
        return redisTemplate.opsForZSet().zCard(queryZsetName);
    }

    /**
     * 入队操作
     * @param storeId 店铺Id
     * @param userId  排队用户Id
     * @return 用户实时排队情况
     */
    public JSONObject push(String storeId, String userId) {

        String queryZsetName = getQueryZsetName(storeId);
        String queryListName = getQueryListName(storeId);
        QueryDetail queryDetail = null;
        QueryUser queryUser = null;
        JSONObject resultJson = new JSONObject();

        if(storeId != null && !storeId.equals("") && userId != null && !userId.equals("")) {

            if(! isExistZset(queryZsetName,userId)) {
                double score = getMaxScore(storeId) + 1.0;
                redisTemplate.opsForZSet().add(queryZsetName,userId,score);
                queryDetail = new QueryDetail();
                queryDetail.setQueryName(queryZsetName);
                queryDetail.setQueryNo(String.valueOf((int) score));
                queryDetail.setQueryUserId(userId);
                String queryDetailStr =  JSONObject.toJSON(queryDetail).toString();
                redisTemplate.opsForList().rightPush(queryListName,queryDetailStr);

                queryUser = new QueryUser();
                queryUser.setQueryNo(String.valueOf((int) score));
                queryUser.setTotalNum(String.valueOf(curQueryNumber(queryZsetName)));
                queryUser.setQueryIndex(String.valueOf(redisTemplate.opsForZSet().rank(queryZsetName,userId) + 1));
                queryUser.setBeforeNum(String.valueOf(redisTemplate.opsForZSet().rank(queryZsetName,userId)));
                resultJson = (JSONObject) JSONObject.toJSON(queryUser);
                return resultJson;
            } else {
                resultJson.put("msg","该用户已在队列中,请勿重新排队!");
                return  resultJson;
            }
        } else {
            resultJson.put("msg","店铺ID和用户ID不能为空!");
            return  resultJson;
        }
    }

    /**
     * 出队-可随机从任意位置出队
     * @param storeId
     * @param userId
     * @return 返回用户的详细排队数据
     */

    /**
     * 实时排队情况
     * @param storeId
     * @param userId
     * @return
     */
    public JSONObject realTimeQuery(String storeId,String userId) {
        String queryZsetName = getQueryZsetName(storeId);
        QueryUser queryUser = null;
        JSONObject resultJson = new JSONObject();

        if(storeId != null && !storeId.equals("") && userId != null && !userId.equals("")) {

            if(isExistZset(queryZsetName,userId)) {
                queryUser = new QueryUser();
                double score = redisTemplate.opsForZSet().score(queryZsetName,userId);
                queryUser.setTotalNum(String.valueOf(curQueryNumber(queryZsetName)));
                queryUser.setQueryIndex(String.valueOf(redisTemplate.opsForZSet().rank(queryZsetName,userId) + 1));
                queryUser.setBeforeNum(String.valueOf(redisTemplate.opsForZSet().rank(queryZsetName,userId)));
                queryUser.setQueryNo(String.valueOf((int) score));
                resultJson = (JSONObject) JSONObject.toJSON(queryUser);
                return resultJson;
            } else {
                resultJson.put("msg","未查到该用户的排队情况,请先排队!");
                return resultJson;
            }
        } else {
            resultJson.put("msg","店铺ID和用户ID不能为空!");
            return  resultJson;
        }
    }

    public JSONObject pop(String storeId, String userId) {

        JSONObject resultJson = new JSONObject();
        if(storeId != null && !storeId.trim().equals("") && userId != null && !userId.trim().equals("")) {
            String queryZsetName = getQueryZsetName(storeId);
            String queryListName = getQueryListName(storeId);

            if(isExistZset(queryZsetName,userId)) {
                long currIndex = redisTemplate.opsForZSet().rank(queryZsetName,userId);
                String removeValue = (String) redisTemplate.opsForList().index(queryListName,currIndex);
                redisTemplate.opsForZSet().remove(queryZsetName,userId);  // 移除Zset的用户排队数据
                redisTemplate.opsForList().remove(queryListName,1,removeValue); // 移除list中的用户排队详情
                resultJson = JSONObject.parseObject(removeValue);
                return resultJson;
            } else {
                resultJson.put("msg","未查到该用户的排队情况,请先排队!");
                return resultJson;
            }
        } else {
            resultJson.put("msg","店铺ID和用户ID不能为空!");
            return resultJson;
        }
    }

    /**
     * 清空一家店铺的排队
     * @param storeId
     * @return
     */
    public JSONObject emptyQuery(String storeId) {
        JSONObject resultJson = new JSONObject();
        if(storeId != null && !storeId.equals("")) {
            String queryZsetName = getQueryZsetName(storeId);
            String queryListName = getQueryListName(storeId);

            boolean result1 = redisTemplate.delete(queryZsetName);
            boolean result2 = redisTemplate.delete(queryListName);

            if(result1 && result2) {
                resultJson.put("msg","排队数据已清空!");
                return resultJson;
            } else {
                resultJson.put("msg","该商家的无排队数据!");
                return resultJson;
            }
        } else {
            resultJson.put("msg","店铺ID不能为空!");
            return resultJson;
        }
    }

    /**
     * 排队情况分页查询
     * @param page
     * @param limit
     * @param storeId
     * @return
     */
    public JSONObject findByPage(Integer page, Integer limit, String storeId) {
        JSONObject resultJson = new JSONObject();
        JSONArray dataJson = new JSONArray();
        Integer currentPage = (page==null||page<1)?1:page;
        Integer pageSize = (limit==null||limit<1)?10:limit;

        if(storeId != null && !storeId.trim().equals("")) {
            String queryListName = getQueryListName(storeId);
            long totalNum = redisTemplate.opsForList().size(queryListName);

            Integer startIndex = pageSize * (currentPage - 1);
            Integer endIndex = pageSize * currentPage - 1;

            List<String> queryDetailStrList = redisTemplate.opsForList().range(queryListName,startIndex,endIndex);
            List<QueryDetail> queryDetailList = new ArrayList<>();
            for(int i = 0; i < queryDetailStrList.size(); i++) {
                JSONObject queryDetailJson = JSONObject.parseObject(queryDetailStrList.get(i));
                QueryDetail queryDetail = new QueryDetail();
                queryDetail.setQueryName(queryDetailJson.get("queryName").toString());
                queryDetail.setQueryUserId(queryDetailJson.get("queryUserId").toString());
                queryDetail.setQueryNo(queryDetailJson.get("queryNo").toString());
                queryDetail.setQueryTime(queryDetailJson.get("queryTime").toString());
                queryDetailList.add(queryDetail);
            }

            dataJson = JSONArray.parseArray(JSON.toJSONString(queryDetailList));
            resultJson.put("msg","查询成功!");
            resultJson.put("total",queryDetailList.size());
            resultJson.put("data",dataJson);
            return resultJson;
        } else {
            resultJson.put("msg","查询失败,店铺ID不能为空!");
            resultJson.put("total",0);
            resultJson.put("data",null);
            return resultJson;
        }
    }
}

controller层代码

import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @author YudaBao
 * @date 2021/2/23 18:03
 */

@RestController
public class QueueController {


    @Autowired
    private RedisQueryService redisQueryService;

    @RequestMapping("/push")
    public JSONObject push(String storeId, String userId) {
        return redisQueryService.push(storeId,userId);
    }

    @RequestMapping("/pop")
    public JSONObject pop(String storeId, String userId) {
        return  redisQueryService.pop(storeId,userId);
    }


    @RequestMapping("/realTimeQuery")
    public JSONObject realTimeQuery(String storeId, String userId) {
        return  redisQueryService.realTimeQuery(storeId,userId);
    }

    @RequestMapping(value = "/findByPage")
    public JSONObject findByPage(Integer page, Integer limit, String storeId) {
        return redisQueryService.findByPage(page, limit,storeId);
    }

    @RequestMapping(value = "/emptyQuery")
    public JSONObject emptyQuery(String storeId) {
        return redisQueryService.emptyQuery(storeId);
    }

}