Java基于Redis实现“附近的人”(含源码下载)

原创 2017年09月13日 09:05:18

“附近的人”在社交类APP已成为标配的功能,Low一点的实现方式可以把坐标存至关系型数据库,通过计算的坐标点距离实现,这种计算可行但计算速度远不及内存操作级别的NoSql数据库。

基于Redis数据库实现附近的人信息缓存,服务由Spring-boot框架搭建。

控制器(Controller)类

@RestController
public class Controller {

    @Autowired
    private NearbyBiz nearbyBiz;

    @RequestMapping
    public String helloWord() {
        return "HelloWord";
    }

    // 附近的人
    @RequestMapping(value = "nearby")
    public Result<List<NearbyBO>> nearby(@Valid NearbyPO paramObj) {
        return nearbyBiz.nearby(paramObj);
    }
}

业务类

@Service
public class NearbyBiz {

    /** 2017-09-01 毫秒值/1000 (秒) **/
    private static final int BASE_SORT_NUM = 1504195200;

    /** 最大距离 **/
    private static final int MAX_DISTANCE = 3000;

    /** 8小时(秒) **/
    private static final int EIGHT_HOUR_SECOND = 60 * 60 * 8;

    /** 附近的人缓存key值,p1-城市编号,p2-地区编号 **/
    private static final String NEARBY_CACHE_KEY = "nearby_%s_%s";

    /** 附近的人用户缓存key值,p1-城市编号,p2-地区编号,p3-用户id **/
    private static final String NEARBY_USER_CACHE_KEY = "nearby_user_%s_%s_%s";

    @Autowired
    private RedisDao redisDao;

    // 线程池
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    // 附近的人
    public Result<List<NearbyBO>> nearby(NearbyPO paramObj) {
        int nowSortNum = (int) (new Date().getTime() / 1000);
        // 此处仅为了减低排序的序号( 获取缓存集合最大排序下标)
        int endIndex = nowSortNum - BASE_SORT_NUM;

        // 缓存key值
        String cacheKey = String.format(NEARBY_CACHE_KEY, paramObj.getCityCode(), paramObj.getAdCode());

        // 取同一城市地区&&八小时区间范围数据(八小时之前缓存数据会删除)
        Set<String> redisNearby = redisDao.getSetByKeyAndScore(cacheKey, endIndex - EIGHT_HOUR_SECOND, endIndex);

        // 开启新线程写入数据(让主线程“专心”处理主业务)
        threadPoolTaskExecutor.execute(new InsertCache(paramObj, cacheKey, endIndex));

        if (redisNearby.size() == 0)
            return new Result<List<NearbyBO>>(false, "附近查无用户", null);

        List<NearbyBO> result = new ArrayList<NearbyBO>(redisNearby.size());
        boolean oneself = true;
        for (String item : redisNearby) {
            NearbyPO cacheNearby = JSONObject.parseObject(item, NearbyPO.class);
            // 缓存里可能有用户自己
            if (cacheNearby.getId().intValue() == paramObj.getId())
                continue;
            double distance = countDistance(paramObj.getLongitude(), paramObj.getLatitude(), cacheNearby.getLongitude(),
                    cacheNearby.getLatitude());
            // 大于限定距离
            if (distance > MAX_DISTANCE)
                continue;
            result.add(new NearbyBO(cacheNearby.getId(), cacheNearby.getName(), distance));
            oneself = false;
        }
        if (oneself)
            return new Result<List<NearbyBO>>(false, "附近查无用户", null);
        return new Result<List<NearbyBO>>(true, "获取成功", result);
    }

    // 把用户定位信息写入缓存
    private class InsertCache implements Runnable {
        // 用户提交的最新坐标信息
        private NearbyPO paramObj;
        // “附近的人”缓存集合key
        private String cacheKey;
        // 获取缓存集合最大排序下标
        private Integer endIndex;

        public InsertCache(NearbyPO paramObj, String cacheKey, Integer endIndex) {
            this.paramObj = paramObj;
            this.cacheKey = cacheKey;
            this.endIndex = endIndex;
        }

        @Override
        public void run() {
            String userCacheKey = String.format(NEARBY_USER_CACHE_KEY, paramObj.getCityCode(), paramObj.getAdCode(),
                    paramObj.getId());
            String cacheNewData = JSONObject.toJSONString(paramObj);
            String cacheUserPosition = redisDao.getOneStringByKey(userCacheKey);
            // 确保用户坐标信息缓存清除慢于“附近的人”坐标信息
            redisDao.setOneStringByKey(userCacheKey, cacheNewData, EIGHT_HOUR_SECOND + 60);

            // 保存用户坐标信息至“附近的人”缓存集合
            redisDao.addOneStringToZSet(cacheKey, cacheNewData, cacheUserPosition, endIndex);
        }

    }

    /**
     * 计算两经纬度点之间的距离(单位:米)
     * 
     * @param longitude1
     *            坐标1经度
     * @param latitude1
     *            坐标1纬度
     * @param longitude2
     *            坐标2经度
     * @param latitude2
     *            坐标1纬度
     * @return
     */
    private static double countDistance(double longitude1, double latitude1, double longitude2, double latitude2) {
        double radLat1 = Math.toRadians(latitude1);
        double radLat2 = Math.toRadians(latitude2);
        double a = radLat1 - radLat2;
        double b = Math.toRadians(longitude1) - Math.toRadians(longitude2);
        double s = 2 * Math.asin(Math.sqrt(
                Math.pow(Math.sin(a / 2), 2) + Math.cos(radLat1) * Math.cos(radLat2) * Math.pow(Math.sin(b / 2), 2)));
        s = s * 6378137.0;
        s = Math.round(s * 10000) / 10000;
        return s;
    }
}

Redis接口类

public interface RedisDao {

    /**
     * 
     * 根据key值获取String
     * 
     * @param key
     * @return
     */
    public String getOneStringByKey(String key);

    /**
     * 
     * 缓存一个String
     * 
     * @param key
     * @param value
     * @param timeoutSeconds
     */
    public void setOneStringByKey(String key, String value, int timeoutSeconds);

    /**
     * 在获取元素下标区间之外的元素会被删除
     * 
     * @param key
     * @param beginScore
     *            获取元素的排序开始下标
     * @param endScore
     *            获取元素的排序结束下标
     * @return 指定排序下标范围内的元素
     */
    public Set<String> getSetByKeyAndScore(String key, int beginScore, int endScore);

    /**
     * 
     * @param key
     * @param newVal
     *            新值
     * @param oldVal
     *            旧值(非空则删除元素)
     * @param score
     *            排序(使用时间基准值来判断是否删除元素)
     */
    public void addOneStringToZSet(String key, String newVal, String oldVal, double score);

}

Redis实现类

@Repository
public class RedisDaoImpl implements RedisDao {

    @Autowired
    protected RedisTemplate<String, String> redisTemplate;

    @Override
    public String getOneStringByKey(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    @Override
    public void setOneStringByKey(String key, String value, int timeoutSeconds) {
        redisTemplate.opsForValue().set(key, value, timeoutSeconds, TimeUnit.SECONDS);
    }

    @Override
    public Set<String> getSetByKeyAndScore(String key, int beginScore, int endScore) {
        redisTemplate.opsForZSet().removeRangeByScore(key, 1, beginScore - 1);
        return redisTemplate.opsForZSet().rangeByScore(key, beginScore, endScore);
    }

    @Override
    public void addOneStringToZSet(String key, String newVal, String oldVal, double score) {
        if (oldVal != null)
            redisTemplate.opsForZSet().remove(key, oldVal);
        redisTemplate.opsForZSet().add(key, newVal, score);
    }
}

入参类(省略get,set方法)

public class NearbyPO {
    @NotNull(message = "id值不能为空")
    private Integer id;
    @NotBlank(message = "名称不能为空")
    private String name;
    @NotNull(message = "城市编码不能为空")
    private Integer cityCode;
    @NotNull(message = "地区编码不能为空")
    private Integer adCode;
    @NotNull(message = "经度不能为空")
    private Double longitude;
    @NotNull(message = "纬度不能为空")
    private Double latitude;
}

出参类(省略get,set方法)

public class NearbyBO {
    //用户id
    private Integer id;
    //用户名称
    private String name;
    //距离
    private Double distance;
}

出参统一封装类(省略get,set方法)

public class Result<T> {
    private boolean success = true;
    private String msg = "";
    private T data = null;

    public Result() {
        super();
    }

    public Result(boolean success) {
        super();
        this.success = success;
    }

    public Result(boolean success, T data) {
        super();
        this.success = success;
        this.data = data;
    }

    public Result(boolean success, String msg, T data) {
        super();
        this.success =