Java实现附近的人案例

wen java案例 2

本文目录导读:

Java实现附近的人案例

  1. 方案一:基于MySQL的简单实现
  2. 方案二:基于Redis Geo的优化实现
  3. 方案三:基于MongoDB的实现
  4. 方案四:完整业务实现
  5. 方案五:高性能优化
  6. 使用建议

我来提供一个完整的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

优化策略

  1. 使用缓存减少数据库查询
  2. 采用网格算法缩小搜索范围
  3. 使用地理位置索引
  4. 定期清理过期位置数据
  5. 使用分区表存储历史位置数据

注意事项

  • 经纬度精度控制
  • 处理数据一致性
  • 考虑用户隐私保护
  • 设置合理的搜索范围限制
  • 做好异常处理和数据校验

这个完整的案例提供了多种实现方案,可以根据实际业务需求选择合适的技术方案。

抱歉,评论功能暂时关闭!