在 Java 中利用 redis 实现 LBS 服务

前言

LBS(基于位置的服务) 服务是现在移动互联网中比较常用的功能。例如外卖服务中常用的我附近的店铺的功能,通常是以用户当前的位置坐标为基础,查询一定距离范围类的店铺,按照距离远近进行倒序排序。

自从 redis 4 版本发布后, lbs 相关命令正式内置在 redis 的发行版中。要实现上述的功能,主要用到 redis geo 相关的两个命令
GEOADD 和 GEORADIOUS

命令描述

  1. GEOADD
    GEOADD key longitude latitude member [longitude latitude member ...]
    这个命令将指定的地理空间位置(纬度、经度、名称)添加到指定的 key 中。
    有效的经度从-180度到180度。
    有效的纬度从-85.05112878度到85.05112878度。
    当坐标位置超出上述指定范围时,该命令将会返回一个错误。
    该命令可以一次添加多个地理位置点

  2. GEORADIOUS

GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count]

这个命令以给定的经纬度为中心, 返回键包含的位置元素当中, 与中心的距离不超过给定最大距离的所有位置元素。
范围可以使用以下其中一个单位:

  • m 表示单位为米。
  • km 表示单位为千米。
  • mi 表示单位为英里。
  • ft 表示单位为英尺。

在给定以下可选项时, 命令会返回额外的信息:

  • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。 距离的单位和用户给定的范围单位保持一致。
  • WITHCOORD: 将位置元素的经度和维度也一并返回。
  • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
  • ASC: 根据中心的位置, 按照从近到远的方式返回位置元素。
  • DESC: 根据中心的位置, 按照从远到近的方式返回位置元素。
  • 在默认情况下, GEORADIUS 命令会返回所有匹配的位置元素。 虽然用户可以使用 COUNT <count> 选项去获取前 N 个匹配元素

接口定义

package com.x9710.common.redis;

import com.x9710.common.redis.domain.GeoCoordinate;
import com.x9710.common.redis.domain.Postion;

import java.util.List;

public interface LBSService {

/**
 * 存储一个位置
 *
 * @param postion 增加的位置对象
 * @throws Exception
 */
boolean addPostion(Postion postion);

/**
 * 查询以指定的坐标为中心,指定的距离为半径的范围类的所有位置点
 *
 * @param center   中心点位置
 * @param distinct 最远距离,单位米
 * @param asc      是否倒序排序
 * @return 有效的位置
 */
List<Postion> radious(String type, GeoCoordinate center, Long distinct, Boolean asc);
}

实现的接口

package com.x9710.common.redis.impl;

import com.x9710.common.redis.LBSService;
import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.domain.GeoCoordinate;
import com.x9710.common.redis.domain.Postion;
import redis.clients.jedis.GeoRadiusResponse;
import redis.clients.jedis.GeoUnit;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.params.geo.GeoRadiusParam;

import java.util.ArrayList;
import java.util.List;

public class LBSServiceRedisImpl implements LBSService {
private RedisConnection redisConnection;

private Integer dbIndex;

public void setRedisConnection(RedisConnection redisConnection) {
    this.redisConnection = redisConnection;
}

public void setDbIndex(Integer dbIndex) {
    this.dbIndex = dbIndex;
}

public boolean addPostion(Postion postion) {
    Jedis jedis = redisConnection.getJedis();
    try {
        return (1L == jedis.geoadd(postion.getType(),
                postion.getCoordinate().getLongitude(),
                postion.getCoordinate().getLatitude(),
                postion.getId()));
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }
}

public List<Postion> radious(String type, GeoCoordinate center, Long distinct, Boolean asc) {
    List<Postion> postions = new ArrayList<Postion>();
    Jedis jedis = redisConnection.getJedis();
    try {
        GeoRadiusParam geoRadiusParam = GeoRadiusParam.geoRadiusParam().withCoord().withDist();
        if (asc) {
            geoRadiusParam.sortAscending();
        } else {
            geoRadiusParam.sortDescending();
        }
        List<GeoRadiusResponse> responses = jedis.georadius(type,
                center.getLongitude(),
                center.getLatitude(),
                distinct.doubleValue(),
                GeoUnit.M,
                geoRadiusParam);
        if (responses != null) {
            for (GeoRadiusResponse response : responses) {
                Postion postion = new Postion(response.getMemberByString(),
                        type,
                        response.getCoordinate().getLongitude(),
                        response.getCoordinate().getLatitude());
                postion.setDistinct(response.getDistance());
                postions.add(postion);
            }
        }
    } finally {
        if (jedis != null) {
            jedis.close();
        }
    }

    return postions;
}
}

测试用例

package com.x9710.common.redis.test;

import com.x9710.common.redis.RedisConnection;
import com.x9710.common.redis.domain.GeoCoordinate;
import com.x9710.common.redis.domain.Postion;
import com.x9710.common.redis.impl.CacheServiceRedisImpl;
import com.x9710.common.redis.impl.LBSServiceRedisImpl;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.util.List;

/**
 * LBS服务测试类
 *
 * @author 杨高超
 * @since 2017-12-28
 */
public class RedisLBSTest {
private CacheServiceRedisImpl cacheService;
private LBSServiceRedisImpl lbsServiceRedis;
private String type = "SHOP";
private GeoCoordinate center;

@Before
public void before() {
    RedisConnection redisConnection = RedisConnectionUtil.create();
    lbsServiceRedis = new LBSServiceRedisImpl();
    lbsServiceRedis.setDbIndex(15);
    lbsServiceRedis.setRedisConnection(redisConnection);
    Postion postion = new Postion("2017122801", type, 91.118970, 29.654210);
    lbsServiceRedis.addPostion(postion);
    postion = new Postion("2017122802", type, 116.373472, 39.972528);
    lbsServiceRedis.addPostion(postion);
    postion = new Postion("2017122803", type, 116.344820, 39.948420);
    lbsServiceRedis.addPostion(postion);
    postion = new Postion("2017122804", type, 116.637920, 39.905460);
    lbsServiceRedis.addPostion(postion);
    postion = new Postion("2017122805", type, 118.514590, 37.448150);
    lbsServiceRedis.addPostion(postion);
    postion = new Postion("2017122806", type, 116.374766, 40.109508);
    lbsServiceRedis.addPostion(postion);
    center = new GeoCoordinate();
    center.setLongitude(116.373472);
    center.setLatitude(39.972528);
}

@Test
public void test10KMRadious() {
    List<Postion> postions = lbsServiceRedis.radious(type, center, 1000 * 10L, true);
    Assert.assertTrue(postions.size() == 2 && exist(postions, "2017122802") && exist(postions, "2017122803"));
}

@Test
public void test50KMRadious() {
    List<Postion> postions = lbsServiceRedis.radious(type, center, 1000 * 50L, true);
    Assert.assertTrue(postions.size() == 4
            && exist(postions, "2017122802")
            && exist(postions, "2017122803")
            && exist(postions, "2017122806")
            && exist(postions, "2017122804"));
}

private boolean exist(List<Postion> postions, String key) {
    if (postions != null) {
        for (Postion postion : postions) {
            if (postion.getId().equals(key)) {
                return true;
            }
        }
    }
    return false;
}

@Before
public void after() {
    RedisConnection redisConnection = RedisConnectionUtil.create();
    cacheService = new CacheServiceRedisImpl();
    cacheService.setDbIndex(15);
    cacheService.setRedisConnection(redisConnection);
    cacheService.delObject(type);
}
}

测试结果

LBS 服务测试结果

后记

这样,我们通过 redis 就能简单实现一个我附近的小店的功能的 LBS服务。

本文程序是在前面的文章《在 Java 中使用 redis》的基础上添加新的实现类的方式完成的。代码同步发布在 GitHub 仓库

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容

  • redis是一个以key-value存储的非关系型数据库。有五种数据类型,string、hashes、list、s...
    林ze宏阅读 962评论 0 0
  • 转自:http://bbs.redis.cn/forum.php?mod=viewthread&tid=481 p...
    木十2036阅读 769评论 0 7
  • 阳光明媚的早晨,姑娘小洁对着镜子梳妆打扮一番,临出门还不忘对着镜子做个鬼脸,像一只快乐的小鸟一样飞出小区。今天一大...
    十个手指阅读 179评论 0 2
  • 朋友有一心爱的杯子,到哪喝茶都带着,于是帮他做了个皮筒装上,做完拿我10块钱的杯子试了下,还行。
    6d4216ce7460阅读 256评论 1 2
  • 感恩一觉醒来呼吸尚在,感恩生命本身的恩赐*感恩情绪波动给我带来自省和成长*感恩母亲的包容和爱*感恩营养美味的素食早...
    晴晴zhang阅读 147评论 0 1