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

2017-09-13 by qq_19260029

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

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

控制器(Controller)类

@RestControllerpublic 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); }}

业务类

@Servicepublic 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实现类

@Repositorypublic 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 = success; this.msg = msg; this.data = data; }}

参考数据

深圳市cityCode:440300
深圳市-福田区adCode:440304
深圳市-南山区adCode:440305

1号用户在深圳南山区定位

http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440305&longitude=113.9572334290&latitude=22.5829485425

Redis缓存

把1号用户定位信息缓存至“深圳市-南山区”附近的人集合(nearby_440300_440305【固定前缀+城市编号+区编号】),并保存用户当前的定位信息(TTL为8小时+60秒,有效保存用户最新定位信息的同时设置了过期时间,为缓存数据库的过期数据提供支持)

请求结果

{"success":false,"msg":"附近查无用户","data":null}

当前深圳市南山区只有1号用户使用附近的人,所以查无用户

2号用户在深圳南山区定位

http://localhost:8080/nearby?id=2&name=2号用户&cityCode=440300&adCode=440305&longitude=113.9582334290&latitude=22.5829485425

Redis缓存

把2号用户定位信息追加至“深圳市-南山区”附近的人集合,并保存2号用户当前的定位信息

请求结果

{"success":true,"msg":"获取成功","data":[{"id":1,"name":"1号用户","distance":102.0}]}

匹配到1号用户,距离为102米

1号用户在深圳福田区定位

http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440304&longitude=114.0180015564&latitude=22.5471230766

Redis缓存

把1号用户定位信息缓存至“深圳市-福田区”附近的人集合,并保存1号用户在福田区的定位信息;不影响1号用户在南山区附近的人缓存信息

请求结果

{"success":false,"msg":"附近查无用户","data":null}

福田区当前只有1号用户定位,所以查无附近的人

1号用户在深圳南山区再次定位请求

http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440305&longitude=113.9572334290&latitude=22.5829485425

Redis缓存

1号用户在南山区重新定位,刷新定位信息(nearby_user_440300_440305_1【固定前缀+城市编号+区编号+用户id】)

请求结果

{"success":true,"msg":"获取成功","data":[{"id":2,"name":"2号用户","distance":102.0}]}}

2号用户附近的人定位信息并没过期(缓存8小时),附近的人匹配到2号用户

深圳南山区“附近的人”集合Redis缓存信息

源码下载(3分)http://download.csdn.net/download/qq_19260029/9976148

最新更新:

第七城市

栏目导航(关闭)