本文目录导读:

我来提供一个完整的Java实现"附近的人"功能的案例,包括多种实现方案。
基于MySQL的简单实现
数据模型
@Entity
@Table(name = "user_location")
public class UserLocation {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "user_name")
private String userName;
@Column(name = "longitude", nullable = false)
private Double longitude;
@Column(name = "latitude", nullable = false)
private Double latitude;
@Column(name = "update_time")
private LocalDateTime updateTime;
// getter/setter...
}
附近的人查询实现
@Service
public class NearbyUserService {
@Autowired
private UserLocationRepository locationRepository;
// 地球半径(公里)
private static final double EARTH_RADIUS_KM = 6371.0;
/**
* 查找附近的人(使用Haversine公式)
*/
public List<UserLocation> findNearbyUsers(double longitude, double latitude, double distanceKm, int limit) {
// 计算经纬度范围(粗略过滤)
double deltaLat = distanceKm / 110.574;
double deltaLng = distanceKm / (111.320 * Math.cos(Math.toRadians(latitude)));
double minLat = latitude - deltaLat;
double maxLat = latitude + deltaLat;
double minLng = longitude - deltaLng;
double maxLng = longitude + deltaLng;
// 先按范围查询,再精确计算距离
List<UserLocation> candidates = locationRepository.findByLatitudeBetweenAndLongitudeBetween(
minLat, maxLat, minLng, maxLng);
// 精确计算距离并按距离排序
return candidates.stream()
.map(user -> {
user.setDistance(calculateDistance(latitude, longitude,
user.getLatitude(), user.getLongitude()));
return user;
})
.filter(user -> user.getDistance() <= distanceKm)
.sorted(Comparator.comparing(UserLocation::getDistance))
.limit(limit)
.collect(Collectors.toList());
}
/**
* 计算两个坐标点之间的距离(Haversine公式)
*/
public static double calculateDistance(double lat1, double lng1, double lat2, double lng2) {
double dLat = Math.toRadians(lat2 - lat1);
double dLng = Math.toRadians(lng2 - lng1);
double a = Math.sin(dLat / 2) * Math.sin(dLat / 2)
+ Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2))
* Math.sin(dLng / 2) * Math.sin(dLng / 2);
double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS_KM * c;
}
}
Repository接口
@Repository
public interface UserLocationRepository extends JpaRepository<UserLocation, Long> {
List<UserLocation> findByLatitudeBetweenAndLongitudeBetween(
double minLat, double maxLat, double minLng, double maxLng);
}
基于Redis Geo的优化实现
Redis配置
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory factory) {
return new StringRedisTemplate(factory);
}
}
Redis Geo服务
@Service
public class RedisGeoService {
private static final String GEO_KEY = "user:geo";
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 添加用户位置
*/
public void addUserLocation(Long userId, double longitude, double latitude) {
redisTemplate.opsForGeo().add(GEO_KEY,
new Point(longitude, latitude),
String.valueOf(userId));
}
/**
* 查找附近的人
*/
public List<NearbyUserDTO> findNearbyUsers(double longitude, double latitude,
double distanceKm, int limit, boolean sortByDistance) {
// 使用Redis GEO搜索
Circle circle = new Circle(new Point(longitude, latitude),
new Distance(distanceKm, RedisGeoCommands.DistanceUnit.KILOMETERS));
RedisGeoCommands.GeoRadiusCommandArgs args =
RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.limit(limit);
if (sortByDistance) {
args.sortAscending();
} else {
args.sortDescending();
}
GeoResults<RedisGeoCommands.GeoLocation<String>> results =
redisTemplate.opsForGeo().radius(GEO_KEY, circle, args);
// 转换结果
List<NearbyUserDTO> nearbyUsers = new ArrayList<>();
if (results != null) {
for (GeoResult<RedisGeoCommands.GeoLocation<String>> result : results) {
RedisGeoCommands.GeoLocation<String> location = result.getContent();
RedisGeoCommands.Distance distance = result.getDistance();
NearbyUserDTO dto = new NearbyUserDTO();
dto.setUserId(Long.parseLong(location.getName()));
dto.setDistance(distance.getValue());
dto.setLongitude(location.getPoint().getX());
dto.setLatitude(location.getPoint().getY());
nearbyUsers.add(dto);
}
}
return nearbyUsers;
}
/**
* 计算两个用户之间的距离
*/
public double getDistance(Long userId1, Long userId2) {
Distance distance = redisTemplate.opsForGeo().distance(GEO_KEY,
String.valueOf(userId1), String.valueOf(userId2),
RedisGeoCommands.DistanceUnit.KILOMETERS);
return distance != null ? distance.getValue() : -1;
}
/**
* 删除用户位置
*/
public void removeUser(Long userId) {
redisTemplate.opsForGeo().remove(GEO_KEY, String.valueOf(userId));
}
/**
* 获取用户位置
*/
public Point getUserLocation(Long userId) {
List<Point> points = redisTemplate.opsForGeo().position(GEO_KEY,
String.valueOf(userId));
return points != null && !points.isEmpty() ? points.get(0) : null;
}
}
DTO对象
public class NearbyUserDTO {
private Long userId;
private String userName;
private Double distance;
private Double longitude;
private Double latitude;
private String avatar;
// getter/setter...
}
基于MongoDB的实现
实体类
@Document(collection = "user_locations")
public class UserGeoLocation {
@Id
private Long userId;
private String userName;
// MongoDB地理位置字段
@GeoSpatialIndexed(type = GeoSpatialIndexType.GEO_2DSPHERE)
private Point location;
@Field("update_time")
private LocalDateTime updateTime;
// getter/setter...
}
Repository
public interface UserGeoRepository extends MongoRepository<UserGeoLocation, Long> {
/**
* 查找附近的用户
*/
@Query("{location: {$nearSphere: {$geometry: {type: 'Point', coordinates: [?0, ?1]}, $maxDistance: ?2}}}")
List<UserGeoLocation> findNearbyUsers(double longitude, double latitude, double maxDistance);
/**
* 按距离排序查找
*/
List<UserGeoLocation> findByLocationNear(Point point, Distance maxDistance);
}
完整业务实现
完整服务实现
@Service
public class NearbyUserServiceImpl implements NearbyUserService {
@Autowired
private RedisGeoService redisGeoService;
@Autowired
private UserService userService;
@Autowired
private UserLocationRepository locationRepository;
/**
* 更新用户位置
*/
@Transactional
public void updateLocation(Long userId, double longitude, double latitude) {
// 1. 保存到MySQL(用于历史记录)
UserLocation userLocation = locationRepository.findByUserId(userId);
if (userLocation != null) {
userLocation.setLongitude(longitude);
userLocation.setLatitude(latitude);
userLocation.setUpdateTime(LocalDateTime.now());
} else {
userLocation = new UserLocation();
userLocation.setUserId(userId);
userLocation.setLongitude(longitude);
userLocation.setLatitude(latitude);
userLocation.setUpdateTime(LocalDateTime.now());
}
locationRepository.save(userLocation);
// 2. 更新到Redis(用于快速查询)
redisGeoService.addUserLocation(userId, longitude, latitude);
}
/**
* 获取附近的人列表
*/
public List<NearbyUserDTO> getNearbyUsers(double longitude, double latitude,
double distance, int limit, String otherParams) {
// 1. 从Redis获取附近的用户ID和距离
List<NearbyUserDTO> nearbyUsers = redisGeoService.findNearbyUsers(
longitude, latitude, distance, limit, true);
// 2. 补充用户详细信息
for (NearbyUserDTO dto : nearbyUsers) {
User user = userService.getUserById(dto.getUserId());
if (user != null) {
dto.setUserName(user.getUserName());
dto.setAvatar(user.getAvatar());
dto.setGender(user.getGender());
dto.setAge(calculateAge(user.getBirthday()));
}
}
return nearbyUsers;
}
/**
* 批量更新位置
*/
@Async
public void batchUpdateLocation(List<UserLocation> locations) {
locations.stream().forEach(location -> {
redisGeoService.addUserLocation(location.getUserId(),
location.getLongitude(), location.getLatitude());
});
}
}
缓存策略
@Service
public class LocationCacheService {
private static final String NEARBY_USERS_CACHE_KEY = "nearby:users:";
private static final Duration CACHE_TTL = Duration.ofMinutes(5);
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 缓存附近的人结果
*/
public void cacheNearbyUsers(String key, List<NearbyUserDTO> users) {
String cacheKey = NEARBY_USERS_CACHE_KEY + key;
redisTemplate.opsForValue().set(cacheKey, users, CACHE_TTL);
}
/**
* 获取缓存的附近的人
*/
public List<NearbyUserDTO> getCachedNearbyUsers(String key) {
String cacheKey = NEARBY_USERS_CACHE_KEY + key;
Object cached = redisTemplate.opsForValue().get(cacheKey);
return cached != null ? (List<NearbyUserDTO>) cached : null;
}
/**
* 生成缓存key(使用网格算法)
*/
public String generateCacheKey(double latitude, double longitude, int zoom) {
// 使用GeoHash或网格ID作为缓存key
String geoHash = GeoHashUtil.encode(latitude, longitude);
return geoHash.substring(0, Math.min(geoHash.length(), zoom));
}
}
控制器层
@RestController
@RequestMapping("/api/nearby")
public class NearbyUserController {
@Autowired
private NearbyUserService nearbyUserService;
/**
* 更新位置
*/
@PostMapping("/location")
public ApiResponse<Void> updateLocation(@RequestBody LocationRequest request) {
nearbyUserService.updateLocation(request.getUserId(),
request.getLongitude(), request.getLatitude());
return ApiResponse.success();
}
/**
* 查询附近的人
*/
@GetMapping("/users")
public ApiResponse<List<NearbyUserDTO>> getNearbyUsers(
@RequestParam double longitude,
@RequestParam double latitude,
@RequestParam(defaultValue = "5") double distance,
@RequestParam(defaultValue = "20") int limit) {
List<NearbyUserDTO> users = nearbyUserService.getNearbyUsers(
longitude, latitude, distance, limit, null);
return ApiResponse.success(users);
}
/**
* 批量更新位置
*/
@PostMapping("/batch/location")
public ApiResponse<Void> batchUpdateLocation(@RequestBody List<LocationRequest> requests) {
nearbyUserService.batchUpdateLocation(requests);
return ApiResponse.success();
}
}
请求/响应实体
public class LocationRequest {
private Long userId;
private Double longitude;
private Double latitude;
// getter/setter...
}
public class ApiResponse<T> {
private Integer code;
private String message;
private T data;
public static <T> ApiResponse<T> success(T data) {
ApiResponse<T> response = new ApiResponse<>();
response.setCode(200);
response.setMessage("success");
response.setData(data);
return response;
}
// getter/setter...
}
高性能优化
网格算法优化
public class GridLocationService {
// 定义网格大小(约1公里)
private static final double GRID_SIZE_KM = 1.0;
/**
* 计算所在网格ID
*/
public String getGridId(double latitude, double longitude) {
int latGrid = (int) Math.round(latitude / GRID_SIZE_KM);
int lngGrid = (int) Math.round(longitude / GRID_SIZE_KM);
return latGrid + ":" + lngGrid;
}
/**
* 获取相邻网格(包含自身)
*/
public List<String> getAdjacentGrids(String gridId, int radius) {
String[] parts = gridId.split(":");
int latGrid = Integer.parseInt(parts[0]);
int lngGrid = Integer.parseInt(parts[1]);
List<String> grids = new ArrayList<>();
for (int i = -radius; i <= radius; i++) {
for (int j = -radius; j <= radius; j++) {
grids.add((latGrid + i) + ":" + (lngGrid + j));
}
}
return grids;
}
}
性能测试和优化
public class NearbyUserBenchmark {
public static void main(String[] args) {
// 生成测试数据
generateTestData(10000);
// 测试查询性能
long startTime = System.currentTimeMillis();
List<NearbyUserDTO> results = queryNearbyUsers(39.9, 116.4, 5, 20);
long endTime = System.currentTimeMillis();
System.out.println("查询耗时: " + (endTime - startTime) + "ms");
System.out.println("返回结果数: " + results.size());
}
private static void generateTestData(int count) {
Random random = new Random();
for (int i = 0; i < count; i++) {
double lat = 39.9 + random.nextDouble() * 2 - 1;
double lng = 116.4 + random.nextDouble() * 2 - 1;
System.out.println(String.format("INSERT INTO user_location (user_id, longitude, latitude) VALUES (%d, %.6f, %.6f);",
i + 1, lng, lat));
}
}
}
使用建议
方案选型
- 数据量较小(<10万):使用MySQL + 索引优化
- 数据量中等(10万-100万):使用Redis Geo
- 数据量较大(>100万):使用MongoDB Geolocation
优化策略
- 使用缓存减少数据库查询
- 采用网格算法缩小搜索范围
- 使用地理位置索引
- 定期清理过期位置数据
- 使用分区表存储历史位置数据
注意事项
- 经纬度精度控制
- 处理数据一致性
- 考虑用户隐私保护
- 设置合理的搜索范围限制
- 做好异常处理和数据校验
这个完整的案例提供了多种实现方案,可以根据实际业务需求选择合适的技术方案。