[Case] SpringBoot integrates GEO of Redis to realize the function of finding nearby stores

Like when we usually order food from Meituan, we will see how many meters away a merchant is from us. There are also people near QQ, we can see how many meters away the nearby people are from us.

insert image description here

So how do these businesses do it? How to realize the location- based nearby service system.

Before going to understand location-based nearby services, let's take a look at what is GIS technology.

GIS stands for Geographic Information System and is a technology for collecting, storing, analyzing, managing and displaying geospatial data. GIS utilizes computer software and hardware to create, manage, analyze and visualize geographic information, enabling users to better understand and solve geospatial problems.

In short, each location on the map will have a latitude and longitude coordinates. According to this coordinate, we can find out the nearby people, or nearby stores and the like.

The following is the map latitude and longitude positioning system based on Baidu. You can experience it yourself. You give it a latitude and longitude, and it can locate you to a certain point on the map. That is, the current latitude and longitude position.

URL: http://jingweidu.757dy.com/

insert image description here

Now that we understand the concept of latitude and longitude, a new data type added after Redis version 3.2 is a data structure for processing geographic location information.

GEO (Geographical Location) : Store and query geographic location data, and quickly calculate the intersection of distance and location sets.

Application Scenario

  • Location service: The GEO structure can be used to store the location information of users or businesses, and to calculate the distance between users or businesses.
  • Business analysis: GEO structure can be used to visualize the distribution of merchants on the map for market analysis and marketing strategy formulation.
  • Recommendation system: GEO structure can be used to calculate the distance between users and merchants, so as to realize the functions of recommending and ranking the recommended merchants according to nearby merchants. Or it can be used to realize representative practices such as group buying and card coupons, such as enabling merchants to automatically issue coupons to users who are closer to them.

Common commands of Redis's GEO data structure

  • geoadd: Add the coordinates of a geographic location
    • grammarGEOADD key longitude latitude member
  • geopos: Get the coordinates of a geographic location
    • grammarGEOPOS key [member [member ...]]
  • geodist: Get the distance between two geographic locations
    • grammarGEODIST key member1 member2 [M | KM | FT | MI]
    • Range unit: m | km | ft | mi --> meters | kilometers | feet | miles
  • georadius: Get a collection of geographic locations within a specified range based on a given geographic location coordinates
    • grammarGEORADIUS key longitude latitude radius <M | KM | FT | MI>
  • georadiusbymember: According to the given geographic location, get the collection of geographic locations within the specified range.
    • grammarGEORADIUSBYMEMBER key member radius <M | KM | FT | MI>
  • geohash: Get the geohash value of a geographic location
    • grammarGEOHASH key [member [member ...]]

SpringBoot integration case practice

Demand background: A user wants to find his nearby food, using the GEO structure of Redis.

The integration of RedisTemplate in the SpringBoot project will not be introduced here.

<!-- 创建SpringBoot项目加入redis的starter依赖 -->
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

First of all, we need to prepare some business information. We first define a business entity class.

/**
 * 定义商家店铺实体类
 * @author lixiang
 * @date 2023/6/21 09:31
 */
@Data
public class Shop {
    
    

    /**
     * id
     */
    private String id;

    /**
     * 名称
     */
    private String name;

    /**
     * 精度
     */
    private BigDecimal accuracy;

    /**
     * 纬度
     */
    private BigDecimal latitude;

    /**
     * 店铺星级
     */
    private String star;

    /**
     * 评分
     */
    private BigDecimal score;

}

Define the text file of store test data, and put it into the memory by reading the file.

0f8207fd52344b348584f82d7ffef389 淮南牛肉汤 15.361239 20.115126 五星 9.7
c0304660e5be494eaff45ce26fcb9bf9 华莱士.鸡肉汉堡 13.361239 21.115126 四星 8.9
a998f4386fa34e16ba9ccf3f448bab7b 驴肉火烧 18.361239 24.115126 五星 9.5
76e50c6b464740bc888a226687961d0a 谷香煎饼 29.361239 24.115126 五星 9.0
1e84ace9b8c6492db416d6abd982e60d 老王鲜肉饼.砂锅 52.361239 40.115126 五星 9.0
3c9557c45a9f4e51ac3bd3ac39052622 麦多馅饼 51.361239 42.115126 三星 7.8
4a5771f48a4f4c61ba1c0b992989af86 张亮麻辣烫 78.361239 67.115126 五星 9.4
6c1b322c2f2546f4a286f745b3b800c3 农家大烤盘饭 80.361239 -67.115126 五星 9.0
2d577e6196414148a7809a469ded51c0 沙野轻食 -80.361239 -67.115126 三星 7.6
588fa28618b147fa87a904a541e7833b 卷饼王.炸串 70.361239 67.115126 五星 9.6
8247ba41fc2942f5b29e91cd42a7b422 凉皮先生.肉夹馍 29.361239 80.115126 五星 9.6
00de5559ecfc4c419b4e6adef8bffee6 火炉火韩式拌饭 12.361239 10.115126 五星 9.9
29d18fc219ed4ad09bdaf2fe806f796f 南城.黄焖鸡米饭 72.361239 50.115126 三星 7.9
770c8f0bbbb44f259d58d1e5b350fbd4 李大姐水饺 52.361239 42.115126 三星 7.9
b3d5dd8773e6475b9bb16ad25f876afb 田老师烤肉 52.469669 42.225196 三星 7.4
d112f7be99c24142b422633cdf15461b 老家炒饼 52.362239 42.145126 四星 8.4
8d59cae232da485d9cb77f6c6060c929 地摊烤冷面 52.398239 42.416526 四星 8.7
5ade01f108ba4ba884a8ec1d37bdf9bb 卤汁拌饭 83.361239 68.115126 四星 8.0
96f462d9a20f40419f18a0f4936ad099 人民公社大饭菜 20.361239 10.115126 四星 8.8
6b0b5955c6b8444ca49a5a2ba39ab49b 炸串王铁板烧 34.361239 20.115126 五星 9.8

We define two methods, one is to get the list, and the other is to get the merchant information according to the ID.

/**
 * @author lixiang
 * @date 2023/6/21 09:53
 */
public class ShopData {
    
    

    public final static String SHOP_KEY = "shop:location";

    private final static List<Shop> SHOP_LIST;

    static {
    
    
        SHOP_LIST = new ArrayList<>();
        BufferedReader reader;
        try {
    
    
            reader = new BufferedReader(new FileReader("/Users/mac/IdeaProjects/spring-redis-demo/src/main/resources/shop.txt"));
            String line;
            do{
    
    
                line = reader.readLine();
                if (!StringUtils.isEmpty(line)){
    
    
                    String[] split = line.split(" ");
                    Shop shop = new Shop();
                    shop.setId(split[0]);
                    shop.setName(split[1]);
                    shop.setAccuracy(new BigDecimal(split[2]));
                    shop.setLatitude(new BigDecimal(split[3]));
                    shop.setStar(split[4]);
                    shop.setScore(new BigDecimal(split[5]));
                    SHOP_LIST.add(shop);
                }
            }while (line != null);
            reader.close();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    /**
     * 获取数据列表
     * @return
     */
    public static List<Shop> getData(){
    
    
        return SHOP_LIST;
    }

    /**
     * 获取数据map结构,根据ID获取商家信息
     * @return
     */
    public static Map<String,Shop> getDataMap(){
    
    
        return SHOP_LIST.stream().collect(Collectors.toMap(Shop::getId,obj->obj));
    }
}

Tests whether data is entered into the collection.

    public static void main(String[] args) {
    
    
        List<Shop> data = ShopData.getData();
        for (Shop datum : data) {
    
    
            System.out.println(datum);
        }
    }

insert image description here

ok, no problem. Next, we started to write an interface for synchronizing geographic location information to Redis. Encapsulates the operational components of GEO.

/**
 * @author lixiang
 * @date 2023/6/21 11:32
 */
@Component
public class GeoComponent {
    
    

    @Autowired
    private StringRedisTemplate redisTemplate;

    /**
     * 添加成员
     * @param key
     * @param lon 经度
     * @param lat 纬度
     * @param member 成员
     * @return
     */
    public Long geoAdd(String key, double lon, double lat, String member){
    
    
        return redisTemplate.opsForGeo().add(key, new Point(lon, lat), member);
    }

    /**
     * 获取两个成员的距离
     * @param key
     * @param member1
     * @param member2
     * @return
     */
    public Distance geoDist(String key, String member1, String member2){
    
    
        return redisTemplate.opsForGeo().distance(key, member1, member2);
    }

    /**
     * 获取两个成员的距离
     * @param key
     * @param member1
     * @param member2
     * @param metric 度规(枚举)(km、m)
     * @return
     */
    public Distance geoDist(String key, String member1, String member2, Metrics metric){
    
    
        return redisTemplate.opsForGeo().distance(key, member1, member2, metric);
    }

    /**
     * 获取成员经纬度
     * @param key
     * @param members
     * @return
     */
    public List<Point> geoPos(String key, String... members){
    
    
        return redisTemplate.opsForGeo().position(key, members);
    }

    /**
     * 获取某个成员附近(距离范围内)的成员
     * @param key
     * @param member 成员
     * @param v 距离
     * @param metric  度规(枚举)(km、m)
     * @return
     */
    public List<String> geoRadiusByMember(String key, String member, double v, Metrics metric){
    
    
        GeoResults<RedisGeoCommands.GeoLocation<String>> geoResults = redisTemplate.opsForGeo().radius(key, member, new Distance(v, metric));
        List<String> result = new ArrayList<>();
        for(GeoResult<RedisGeoCommands.GeoLocation<String>> geoResult :geoResults.getContent()){
    
    
            result.add(geoResult.getContent().getName());
        }
        return result;
    }

    /**
     * 获取某个成员附近(距离范围内)的成员
     * @param key
     * @param member 成员
     * @param v 距离
     * @param metric  度规(枚举)(km、m)
     * @param args
     * 示例:RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeCoordinates().includeDistance().limit(1).sortAscending();
     * includeCoordinates:结果包含坐标,includeDistance:结果包含距离,limit:返回数量:sort...:排序
     * @return GeoResults
     * geoResult.getContent().getName() 元素名称
     * geoResult.getContent().getPoint() 元素坐标
     * geoResult.getDistance() 元素距离
     */
    public GeoResults<RedisGeoCommands.GeoLocation<String>> geoRadiusByMember(String key, String member, double v, Metrics metric, RedisGeoCommands.GeoRadiusCommandArgs args){
    
    
        return redisTemplate.opsForGeo().radius(key, member, new Distance(v, metric), args);
    }
}

Define the ShopService-syncShopLocationData() method

		@Override
    public void syncShopLocationData() {
    
    
        List<Shop> data = ShopData.getData();
        //地理位置信息同步到Redis
        data.forEach(obj->{
    
    
            double accuracy = obj.getAccuracy().doubleValue();
            double latitude = obj.getLatitude().doubleValue();
            String id = obj.getId();
            geoComponent.geoAdd(SHOP_KEY,accuracy,latitude,id);
        });
    }
    @Autowired
    private ShopService shopService;
    private final static String SHOP_KEY = "shop:location";
    @GetMapping("/syncShopLocationToRedis")
    public void syncShopLocationToRedis(){
    
    
        shopService.syncShopLocationData();
    }

Test call, the data has been written to Redis.

insert image description here

ok, the data has been stored. Next, let's realize that according to the user's distance, search for merchants within 10km near the user, and arrange the list of merchants according to the distance from small to large.

Here we first define a method in Service to find merchants by ID. With the method provided by our previous ShopData, writing here is very simple.

		@Override
    public Shop getShopById(String id) {
    
    
        return ShopData.getDataMap().get(id);
    }

Define the controller method, here simulate a user information, give him a latitude and longitude, to find nearby stores.

    @GetMapping("/getShopListByLocation")
    public List<ShopVO> getShopListByLocation(){
    
    
        //模拟用户信息
        Map<String,Object> user = new HashMap<>();
        user.put("accuracy",70.361239);
        user.put("latitude",67.115126);
        user.put("name","李祥");
        user.put("id", UUID.randomUUID().toString().replace("-",""));
        List<ShopVO> shopVO = shopService.getShopListByLocation(user);
        return shopVO;
    }

Define the logic in the service layer.

		@Override
    public List<ShopVO> getShopListByLocation(Map<String, Object> user) {
    
    

        List<ShopVO> shopVOS = new ArrayList<>();

        // 获取用户的坐标位置
        double accuracy = (double) user.get("accuracy");
        double latitude = (double) user.get("latitude");
        String userId = String.valueOf(user.get("id"));

        // 将用户位置加入到Redis
        geoComponent.geoAdd(ShopData.SHOP_KEY, accuracy, latitude, userId);
        // 获取用户附近的门店
        List<String> shopIds = geoComponent.geoRadiusByMember(ShopData.SHOP_KEY, userId, 10, Metrics.KILOMETERS);
        for (String shopId : shopIds) {
    
    
            //如果是当前userId则直接跳出
            if (shopId.equals(userId)) {
    
    
                continue;
            }
            //获取shop信息
            Shop shop = this.getShopById(shopId);
            ShopVO shopVO = new ShopVO();
            BeanUtils.copyProperties(shop, shopVO);
            //获取两点的距离
            double distance = geoComponent.geoDist(ShopData.SHOP_KEY, userId, shopId, Metrics.KILOMETERS).getValue();
            //保留一位小数
            distance = new BigDecimal(distance).setScale(1, BigDecimal.ROUND_DOWN).doubleValue();
            shopVO.setDistance(distance);
            shopVOS.add(shopVO);
        }
        // 删除Redis中用户位置。
        geoComponent.geoDelete(ShopData.SHOP_KEY,userId);
        //排序 返回
        return shopVOS.stream().sorted(Comparator.comparingDouble(ShopVO::getDistance)).collect(Collectors.toList());    
    }

Test verification:

insert image description here

OK, then we will stop here for the practical operation of Redis's GEO data structure. If you think the blogger writes well, remember to give it a like!
insert image description here

Guess you like

Origin blog.csdn.net/weixin_47533244/article/details/131344943