“附近的人”在社交类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> 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> 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 redisNearby = redisDao.getSetByKeyAndScore(cacheKey, endIndex - EIGHT_HOUR_SECOND, endIndex);
// 开启新线程写入数据(让主线程“专心”处理主业务)
threadPoolTaskExecutor.execute(new InsertCache(paramObj, cacheKey, endIndex));
if (redisNearby.size() == 0)
return new Result>(false, "附近查无用户", null);
List result = new ArrayList(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>(false, "附近查无用户", null);
return new Result>(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 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 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 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
http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440305&longitude=113.9572334290&latitude=22.5829485425
把1号用户定位信息缓存至“深圳市-南山区”附近的人集合(nearby_440300_440305【固定前缀+城市编号+区编号】),并保存用户当前的定位信息(TTL为8小时+60秒,有效保存用户最新定位信息的同时设置了过期时间,为缓存数据库的过期数据提供支持)
请求结果
{"success":false,"msg":"附近查无用户","data":null}
当前深圳市南山区只有1号用户使用附近的人,所以查无用户
http://localhost:8080/nearby?id=2&name=2号用户&cityCode=440300&adCode=440305&longitude=113.9582334290&latitude=22.5829485425
把2号用户定位信息追加至“深圳市-南山区”附近的人集合,并保存2号用户当前的定位信息
请求结果
{"success":true,"msg":"获取成功","data":[{"id":1,"name":"1号用户","distance":102.0}]}
匹配到1号用户,距离为102米
http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440304&longitude=114.0180015564&latitude=22.5471230766
把1号用户定位信息缓存至“深圳市-福田区”附近的人集合,并保存1号用户在福田区的定位信息;不影响1号用户在南山区附近的人缓存信息
请求结果
{"success":false,"msg":"附近查无用户","data":null}
福田区当前只有1号用户定位,所以查无附近的人
http://localhost:8080/nearby?id=1&name=1号用户&cityCode=440300&adCode=440305&longitude=113.9572334290&latitude=22.5829485425
1号用户在南山区重新定位,刷新定位信息(nearby_user_440300_440305_1【固定前缀+城市编号+区编号+用户id】)
请求结果
{"success":true,"msg":"获取成功","data":[{"id":2,"name":"2号用户","distance":102.0}]}}
2号用户附近的人定位信息并没过期(缓存8小时),附近的人匹配到2号用户
项目源码下载(3分)http://download.csdn.net/download/qq_19260029/9976148
更多文章:
Spring boot基于Redis缓存商城分类,商品信息
Eclipse新建Spring-boot项目,打包部署并输出HelloWord