当前位置: 首页 > news >正文

活动需求中灵活使用Redis提升生产力

抽奖

一堆用户参与进来,然后随机抽取几个幸运用户给予实物/虚拟的奖品;此时,开发人员就需要写上一个抽奖的算法,来实现幸运用户的抽取;其实我们完全可以利用Redis的集合(Set),就能轻松实现抽奖的功能;

功能实现需要的API

  • SADD key member1 [member2]:添加一个或者多个参与用户;
  • SRANDMEMBER KEY [count]:随机返回一个或者多个用户;
  • SPOP key:随机返回一个或者多个用户,并删除返回的用户;

SRANDMEMBER 和 SPOP 主要用于两种不同的抽奖模式,SRANDMEMBER 适用于一个用户可中奖多次的场景(就是中奖之后,不从用户池中移除,继续参与其他奖项的抽取);而 SPOP 就适用于仅能中一次的场景(一旦中奖,就将用户从用户池中移除,后续的抽奖,就不可能再抽到该用户); 通常 SPOP 会用的会比较多。

Redis-Cli操作

127.0.0.1:6379> SADD raffle user1
(integer) 1
127.0.0.1:6379> SADD raffle user2 user3 user4 user5 user6 user7 user8 user9 user10
(integer) 9
127.0.0.1:6379> SRANDMEMBER raffle 2
1) "user5"
2) "user2"
127.0.0.1:6379> SPOP raffle 2
1) "user3"
2) "user4"
127.0.0.1:6379> SPOP raffle 2
1) "user10"
2) "user9"

代码实现

@Slf4j
@SpringBootTest
public class RaffleMain {private final String KEY_RAFFLE_PROFIX = "raffle:";@AutowiredRedisTemplate redisTemplate;@Testvoid test() {Integer raffleId = 1;join(raffleId, 1000, 1001, 2233, 7890, 44556, 74512);List lucky = lucky(raffleId, 2);log.info("活动:{} 的幸运中奖用户是:{}", raffleId, lucky);}public void join(Integer raffleId, Integer... userIds) {String key = KEY_RAFFLE_PROFIX + raffleId;redisTemplate.opsForSet().add(key, userIds);}public List lucky(Integer raffleId, long num) {String key = KEY_RAFFLE_PROFIX + raffleId;// 随机抽取 抽完之后将用户移除奖池List list = redisTemplate.opsForSet().pop(key, num);// 随机抽取 抽完之后用户保留在池子里//List list = redisTemplate.opsForSet().randomMembers(key, num);return list;}}

点赞收藏

有互动属性活动一般都会有点赞/收藏/喜欢等功能,来提升用户之间的互动。

传统的实现:用户点赞之后,在数据库中记录一条数据,同时一般都会在主题库中记录一个点赞/收藏汇总数,来方便显示;

Redis方案:基于Redis的集合(Set),记录每个帖子/文章对应的收藏、点赞的用户数据,同时set还提供了检查集合中是否存在指定用户,用户快速判断用户是否已经点赞过

功能实现需要的API

  • SADD key member1 [member2]:添加一个或者多个成员(点赞)
  • SCARD key:获取所有成员的数量(点赞数量)
  • SISMEMBER key member:判断成员是否存在(是否点赞)
  • SREM key member1 [member2] :移除一个或者多个成员(点赞数量)

Redis-Cli操作

127.0.0.1:6379> sadd like:article:1 user1
(integer) 1
127.0.0.1:6379> sadd like:article:1 user2
(integer) 1
# 获取成员数量(点赞数量)
127.0.0.1:6379> SCARD like:article:1
(integer) 2
# 判断成员是否存在(是否点赞)
127.0.0.1:6379> SISMEMBER like:article:1 user1
(integer) 1
127.0.0.1:6379> SISMEMBER like:article:1 user3
(integer) 0
# 移除一个或者多个成员(取消点赞)
127.0.0.1:6379> SREM like:article:1 user1
(integer) 1
127.0.0.1:6379> SCARD like:article:1
(integer) 1

代码实现

@Slf4j
@SpringBootTest
public class LikeMain {private final String KEY_LIKE_ARTICLE_PROFIX = "like:article:";@AutowiredRedisTemplate redisTemplate;@Testvoid test() {long articleId = 100;Long likeNum = like(articleId, 1001, 1002, 2001, 3005, 4003);unLike(articleId, 2001);likeNum = likeNum(articleId);boolean b2001 = isLike(articleId, 2001);boolean b3005 = isLike(articleId, 3005);log.info("文章:{} 点赞数量:{} 用户2001的点赞状态:{} 用户3005的点赞状态:{}", articleId, likeNum, b2001, b3005);}/*** 点赞** @param articleId 文章ID* @return 点赞数量*/public Long like(Long articleId, Integer... userIds) {String key = KEY_LIKE_ARTICLE_PROFIX + articleId;Long add = redisTemplate.opsForSet().add(key, userIds);return add;}public Long unLike(Long articleId, Integer... userIds) {String key = KEY_LIKE_ARTICLE_PROFIX + articleId;Long remove = redisTemplate.opsForSet().remove(key, userIds);return remove;}public Long likeNum(Long articleId) {String key = KEY_LIKE_ARTICLE_PROFIX + articleId;Long size = redisTemplate.opsForSet().size(key);return size;}public Boolean isLike(Long articleId, Integer userId) {String key = KEY_LIKE_ARTICLE_PROFIX + articleId;return redisTemplate.opsForSet().isMember(key, userId);}}

排行榜

排名、排行榜、热搜榜是很多活动、游戏都有的功能,常用于用户活动推广、竞技排名、热门信息展示等功能;

比如上面的热搜榜,热度数据来源于全网用户的贡献,但用户只关心热度最高的前50条。

常规的做法:就是将用户的名次、分数等用于排名的数据更新到数据库,然后查询的时候通过Order by + limit 取出前50名显示,如果是参与用户不多,更新不频繁的数据,采用数据库的方式也没有啥问题,但是一旦出现爆炸性热点资讯(比如:大陆收复湾湾,xxx某些绿了等等),短时间会出现爆炸式的流量,瞬间的压力可能让数据库扛不住;

Redis方案:将热点资讯全页缓存,采用Redis的有序队列(Sorted Set)来缓存热度(SCORES),即可瞬间缓解数据库的压力,同时轻松筛选出热度最高的50条;

功能实现需要的命令

  • ZADD key score1 member1 [score2 member2]:添加并设置SCORES,支持一次性添加多个;
  • ZREVRANGE key start stop [WITHSCORES] :根据SCORES降序排列;
  • ZRANGE key start stop [WITHSCORES] :根据SCORES降序排列;

Redis-Cli操作

# 单个插入
127.0.0.1:6379> ZADD ranking 1 user1  
(integer) 1
# 批量插入
127.0.0.1:6379> ZADD ranking 10 user2 50 user3 3 user4 25 user5
(integer) 4
# 降序排列 不带SCORES
127.0.0.1:6379> ZREVRANGE ranking 0 -1 
1) "user3"
2) "user5"
3) "user2"
4) "user4"
5) "user1"
# 降序排列 带SCORES
127.0.0.1:6379> ZREVRANGE ranking 0 -1 WITHSCORES
1) "user3"
2) "50"
3) "user5"
4) "25"
5) "user2"
6) "10"
7) "user4"
8) "3"
9) "user1"
10) "1"
# 升序
127.0.0.1:6379> ZRANGE ranking 0 -1 WITHSCORES
1) "user1"
2) "1"
3) "user4"
4) "3"
5) "user2"
6) "10"
7) "user5"
8) "25"
9) "user3"
10) "50"

代码实现

@SpringBootTest
@Slf4j
public class RankingTest {private final String KEY_RANKING = "ranking";@AutowiredRedisTemplate redisTemplate;@Testvoid test() {add(1001, (double) 60);add(1002, (double) 80);add(1003, (double) 100);add(1004, (double) 90);add(1005, (double) 70);// 取所有Set<DefaultTypedTuple> range = range(0, -1);log.info("所有用户排序:{}", range);// 前三名range = range(0, 2);log.info("前三名排序:{}", range);}public Boolean add(Integer userId, Double score) {Boolean add = redisTemplate.opsForZSet().add(KEY_RANKING, userId, score);return add;}public Set<DefaultTypedTuple> range(long min, long max) {// 降序Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().reverseRangeWithScores(KEY_RANKING, min, max);// 升序//Set<DefaultTypedTuple> set = redisTemplate.opsForZSet().rangeWithScores(KEY_RANKING, min, max);return set;}
}

用户签到(BitMap)

很多活动为了拉动用户活跃度,会加入比如连续签到的功能

传统做法:用户每次签到时,往是数据库插入一条签到数据,展示的时候,把本月(或者指定周期)的签到数据获取出来,用于判断用户是否签到、以及连续签到情况;此方式,简单,理解容易;

Redis做法:由于签到数据的关注点就2个:是否签到(0/1)、连续性,因此就完全可以利用BitMap(位图)来实现;

如上图所示,将一个月的31天,用31个位(4个字节)来表示,偏移量(offset)代表当前是第几天,0/1表示当前是否签到,连续签到只需从右往左校验连续为1的位数;

由于String类型的最大上限是512M,转换为bit则是2^32个bit位。

所需命令:

  • SETBIT key offset value:向指定位置offset存入一个0或1
  • GETBIT key offset:获取指定位置offset的bit值
  • BITCOUNT key [start] [end]:统计BitMap中值为1的bit位的数量
  • BITFIELD: 操作(查询,修改,自增)BitMap中bit 数组中的指定位置offset的值这里最不容易理解的就是:BITFIELD,详情可参考:https://deepinout.com/redis-cmd/redis-bitmap-cmd/redis-cmd-bitfield.html 而且这部分还必须理解了,否则,该需求的核心部分就没办法理解了;

需求:假如当前为8月4号,检测本月的签到情况,用户分别于1、3、4号签到过

Redis-Cli操作

# 8月1号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 0 1
(integer) 1# 8月3号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 2 1
(integer) 1# 8月4号的签到
127.0.0.1:6379> SETBIT RangeId:Sign:1:8899 3 1
(integer) 1# 查询各天的签到情况
# 查询1号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 0
(integer) 1
# 查询2号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 1
(integer) 0
# 查询3号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 2
(integer) 1
# 查询4号
127.0.0.1:6379> GETBIT RangeId:Sign:1:8899 3
(integer) 1# 查询指定区间的签到情况
127.0.0.1:6379> BITFIELD RangeId:Sign:1:8899 get u4 0
1) (integer) 11

1-4号的签到情况为:1011(二进制) ==> 11(十进制)

代码实现-按月签到

@Slf4j
@Service
public class SignByMonthServiceImpl {@AutowiredStringRedisTemplate stringRedisTemplate;private int dayOfMonth() {DateTime dateTime = new DateTime();return dateTime.dayOfMonth().get();}/*** 按照月份和用户ID生成用户签到标识 UserId:Sign:560:2021-08** @param userId 用户id* @return*/private String signKeyWitMouth(String userId) {DateTime dateTime = new DateTime();DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM");StringBuilder builder = new StringBuilder("UserId:Sign:");builder.append(userId).append(":").append(dateTime.toString(fmt));return builder.toString();}/*** 设置标记位* 标记是否签到** @param key* @param offset* @param tag* @return*/public Boolean sign(String key, long offset, boolean tag) {return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);}/*** 统计计数** @param key 用户标识* @return*/public long bitCount(String key) {return stringRedisTemplate.execute((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域*/public List<Long> bitfield(String buildSignKey, int limit, long offset) {return this.stringRedisTemplate.opsForValue().bitField(buildSignKey, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));}/*** 判断是否被标记** @param key* @param offest* @return*/public Boolean container(String key, long offest) {return this.stringRedisTemplate.opsForValue().getBit(key, offest);}/*** 用户今天是否签到** @param userId* @return*/public int checkSign(String userId) {DateTime dateTime = new DateTime();String signKey = this.signKeyWitMouth(userId);int offset = dateTime.getDayOfMonth() - 1;int value = this.container(signKey, offset) ? 1 : 0;return value;}/*** 查询用户当月签到日历** @param userId* @return*/public Map<String, Boolean> querySignedInMonth(String userId) {DateTime dateTime = new DateTime();int lengthOfMonth = dateTime.dayOfMonth().getMaximumValue();Map<String, Boolean> signedInMap = new HashMap<>(dateTime.getDayOfMonth());String signKey = this.signKeyWitMouth(userId);List<Long> bitfield = this.bitfield(signKey, lengthOfMonth, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);DateTimeFormatter fmt = DateTimeFormat.forPattern("yyyy-MM-dd");for (int i = lengthOfMonth; i > 0; i--) {DateTime dateTime1 = dateTime.withDayOfMonth(i);signedInMap.put(dateTime1.toString(fmt), signFlag >> 1 << 1 != signFlag);signFlag >>= 1;}}return signedInMap;}/*** 用户签到** @param userId* @return*/public boolean signWithUserId(String userId) {int dayOfMonth = this.dayOfMonth();String signKey = this.signKeyWitMouth(userId);long offset = (long) dayOfMonth - 1;boolean re = false;if (Boolean.TRUE.equals(this.sign(signKey, offset, Boolean.TRUE))) {re = true;}// 查询用户连续签到次数,最大连续次数为7天long continuousSignCount = this.queryContinuousSignCount(userId, 7);return re;}/*** 统计当前月份一共签到天数** @param userId*/public long countSignedInDayOfMonth(String userId) {String signKey = this.signKeyWitMouth(userId);return this.bitCount(signKey);}/*** 查询用户当月连续签到次数** @param userId* @return*/public long queryContinuousSignCountOfMonth(String userId) {int signCount = 0;String signKey = this.signKeyWitMouth(userId);int dayOfMonth = this.dayOfMonth();List<Long> bitfield = this.bitfield(signKey, dayOfMonth, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);DateTime dateTime = new DateTime();// 连续不为0即为连续签到次数,当天未签到情况下for (int i = 0; i < dateTime.getDayOfMonth(); i++) {if (signFlag >> 1 << 1 == signFlag) {if (i > 0) break;} else {signCount += 1;}signFlag >>= 1;}}return signCount;}/*** 以7天一个周期连续签到次数** @param period 周期* @return*/public long queryContinuousSignCount(String userId, Integer period) {//查询目前连续签到次数long count = this.queryContinuousSignCountOfMonth(userId);//按最大连续签到取余if (period != null && period < count) {long num = count % period;if (num == 0) {count = period;} else {count = num;}}return count;}
}

测试

@SpringBootTest
@Slf4j
public class SignTest2 {@Autowiredprivate SignByMonthServiceImpl signByMonthService;@Autowiredprivate StringRedisTemplate redisTemplate;/*** 测试用户按月签到*/@Testpublic void querySignDay() {//模拟用户签到//for(int i=5;i<19;i++){redisTemplate.opsForValue().setBit("UserId:Sign:560:2022-08", 0, true);//}System.out.println("560用户今日是否已签到:" + this.signByMonthService.checkSign("560"));Map<String, Boolean> stringBooleanMap = this.signByMonthService.querySignedInMonth("560");System.out.println("本月签到情况:");for (Map.Entry<String, Boolean> entry : stringBooleanMap.entrySet()) {System.out.println(entry.getKey() + ": " + (entry.getValue() ? "√" : "-"));}long countSignedInDayOfMonth = this.signByMonthService.countSignedInDayOfMonth("560");System.out.println("本月一共签到:" + countSignedInDayOfMonth + "天");System.out.println("目前连续签到:" + this.signByMonthService.queryContinuousSignCount("560", 7) + "天");}
}

代码实现-指定时间签到

@Slf4j
@Service
public class SignByRangeServiceImpl {@AutowiredStringRedisTemplate stringRedisTemplate;/*** 根据区间的id 以及用户id 拼接key** @param rangeId 区间ID 一般是指定活动的ID等* @param userId  用户的ID* @return*/private String signKey(Integer rangeId, Integer userId) {StringBuilder builder = new StringBuilder("RangeId:Sign:");builder.append(rangeId).append(":").append(userId);return builder.toString();}/*** 获取当前时间与起始时间的间隔天数** @param start 起始时间* @return*/private int intervalTime(LocalDateTime start) {return (int) (LocalDateTime.now().toLocalDate().toEpochDay() - start.toLocalDate().toEpochDay());}/*** 设置标记位* 标记是否签到** @param key    签到的key* @param offset 偏移量 一般是指当前时间离起始时间(活动开始)的天数* @param tag    是否签到  true:签到  false:未签到* @return*/private Boolean setBit(String key, long offset, boolean tag) {return this.stringRedisTemplate.opsForValue().setBit(key, offset, tag);}/*** 统计计数** @param key 统计的key* @return*/private long bitCount(String key) {return stringRedisTemplate.execute((RedisCallback<Long>) redisConnection -> redisConnection.bitCount(key.getBytes()));}/*** 获取多字节位域** @param key    缓存的key* @param limit  获取多少* @param offset 偏移量是多少* @return*/private List<Long> bitfield(String key, int limit, long offset) {return this.stringRedisTemplate.opsForValue().bitField(key, BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(limit)).valueAt(offset));}/*** 判断是否签到** @param key    缓存的key* @param offest 偏移量 指当前时间距离起始时间的天数* @return*/private Boolean container(String key, long offest) {return this.stringRedisTemplate.opsForValue().getBit(key, offest);}/*** 根据起始时间进行签到** @param rangeId* @param userId* @param start* @return*/public Boolean sign(Integer rangeId, Integer userId, LocalDateTime start) {int offset = intervalTime(start);String key = signKey(rangeId, userId);return setBit(key, offset, true);}/*** 根据偏移量签到** @param rangeId* @param userId* @param offset* @return*/public Boolean sign(Integer rangeId, Integer userId, long offset) {String key = signKey(rangeId, userId);return setBit(key, offset, true);}/*** 用户今天是否签到** @param userId* @return*/public Boolean checkSign(Integer rangeId, Integer userId, LocalDateTime start) {long offset = intervalTime(start);String key = this.signKey(rangeId, userId);return this.container(key, offset);}/*** 统计当前月份一共签到天数** @param userId*/public long countSigned(Integer rangeId, Integer userId) {String signKey = this.signKey(rangeId, userId);return this.bitCount(signKey);}public Map<String, Boolean> querySigned(Integer rangeId, Integer userId, LocalDateTime start) {int days = intervalTime(start);Map<String, Boolean> signedInMap = new HashMap<>(days);String signKey = this.signKey(rangeId, userId);List<Long> bitfield = this.bitfield(signKey, days + 1, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);DateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyy-MM-dd");for (int i = days; i >= 0; i--) {LocalDateTime localDateTime = start.plusDays(i);signedInMap.put(localDateTime.format(fmt), signFlag >> 1 << 1 != signFlag);signFlag >>= 1;}}return signedInMap;}/*** 查询用户当月连续签到次数** @param userId* @return*/public long queryContinuousSignCount(Integer rangeId, Integer userId, LocalDateTime start) {int signCount = 0;String signKey = this.signKey(rangeId, userId);int days = this.intervalTime(start);List<Long> bitfield = this.bitfield(signKey, days + 1, 0);if (!CollectionUtils.isEmpty(bitfield)) {long signFlag = bitfield.get(0) == null ? 0 : bitfield.get(0);DateTime dateTime = new DateTime();// 连续不为0即为连续签到次数,当天未签到情况下for (int i = 0; i < dateTime.getDayOfMonth(); i++) {if (signFlag >> 1 << 1 == signFlag) {if (i > 0) break;} else {signCount += 1;}signFlag >>= 1;}}return signCount;}
}

测试

@SpringBootTest
@Slf4j
public class SignTest {@AutowiredSignByRangeServiceImpl signByRangeService;@Testvoid test() {DateTimeFormatter isoDateTime = DateTimeFormatter.ISO_DATE_TIME;// 活动开始时间LocalDateTime start = LocalDateTime.of(2022, 8, 1, 1, 0, 0);Integer rangeId = 1;Integer userId = 8899;log.info("签到开始时间: {}", start.format(isoDateTime));log.info("活动ID: {} 用户ID: {}", rangeId, userId);// 手动指定偏移量签到signByRangeService.sign(rangeId, userId, 0);// 判断是否签到Boolean signed = signByRangeService.checkSign(rangeId, userId, start);log.info("今日是否签到: {}", signed ? "√" : "-");// 签到Boolean sign = signByRangeService.sign(rangeId, userId, start);log.info("签到操作之前的签到状态:{} (-:表示今日第一次签到,√:表示今天已经签到过了)", sign ? "√" : "-");// 签到总数long countSigned = signByRangeService.countSigned(rangeId, userId);log.info("总共签到: {} 天", countSigned);// 连续签到的次数long continuousSignCount = signByRangeService.queryContinuousSignCount(rangeId, userId, start);log.info("连续签到: {} 天", continuousSignCount);// 签到的详情Map<String, Boolean> stringBooleanMap = signByRangeService.querySigned(rangeId, userId, start);for (Map.Entry<String, Boolean> entry : stringBooleanMap.entrySet()) {log.info("签到详情> {} : {}", entry.getKey(), (entry.getValue() ? "√" : "-"));}}
}

GEO搜附近

活动中经常有展示附近用户、商家的诉求;

如果自己想要根据经纬度来实现一个搜索附近的功能,是非常麻烦的;但是Redis 在3.2的版本新增了Redis GEO,用于存储地址位置信息,并对支持范围搜索;基于GEO就能轻松且快速的开发一个搜索附近的功能;

GEO API 及Redis-cli 操作

geoadd:新增位置坐标:

127.0.0.1:6379> GEOADD drinks 116.62445 39.86206 starbucks 117.3514785 38.7501247 yidiandian 116.538542 39.75412 xicha
(integer) 3

geopos:获取位置坐标:

127.0.0.1:6379> GEOPOS drinks starbucks
1) 1) "116.62445157766342163"2) "39.86206038535793539"
127.0.0.1:6379> GEOPOS drinks starbucks yidiandian mxbc
1) 1) "116.62445157766342163"2) "39.86206038535793539"
2) 1) "117.35148042440414429"2) "38.75012383773680114"
3) (nil)

geodist:计算两个位置之间的距离,

单位参数:

  • m :米,默认单位。
  • km :千米。
  • mi :英里。
  • ft :英尺。
127.0.0.1:6379> GEODIST drinks starbucks yidiandian
"138602.4133"
127.0.0.1:6379> GEODIST drinks starbucks xicha
"14072.1255"
127.0.0.1:6379> GEODIST drinks starbucks xicha m
"14072.1255"
127.0.0.1:6379> GEODIST drinks starbucks xicha km
"14.0721"

georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合

参数说明:

    • m :米,默认单位。
    • km :千米。
    • mi :英里。
    • ft :英尺。
    • WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
    • WITHCOORD: 将位置元素的经度和纬度也一并返回。
    • WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
    • COUNT 限定返回的记录数。
    • ASC: 查找结果根据距离从近到远排序。
    • DESC: 查找结果根据从远到近排序。

127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST
1) 1) "xicha"2) "95.8085"
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"
127.0.0.1:6379> GEORADIUS drinks 116 39 100 km WITHDIST WITHCOORD WITHHASH
1) 1) "xicha"2) "95.8085"3) (integer) 40691518008823014) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1 ASC
1) 1) "xicha"2) "95.8085"3) 1) "116.53854042291641235"2) "39.75411928478748536"127.0.0.1:6379> GEORADIUS drinks 116 39 120 km WITHDIST WITHCOORD  COUNT 1 DESC
1) 1) "starbucks"2) "109.8703"3) 1) "116.62445157766342163"2) "39.86206038535793539"

georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。功能和上面的georadius类似,只是georadius是以经纬度坐标为中心,这个是以某个地点为中心;

geohash:返回一个或多个位置对象的 geohash 值。

127.0.0.1:6379> GEOHASH drinks starbucks xicha
1) "wx4fvbem6d0"
2) "wx4f5vhb8b0"

代码实现

@SpringBootTest
@Slf4j
public class GEOTest {private final String KEY = "geo:drinks";@AutowiredRedisTemplate redisTemplate;@Testpublic void test() {add("starbucks", new Point(116.62445, 39.86206));add("yidiandian", new Point(117.3514785, 38.7501247));add("xicha", new Point(116.538542, 39.75412));get("starbucks", "yidiandian", "xicha");GeoResults nearByXY = getNearByXY(new Point(116, 39), new Distance(120, Metrics.KILOMETERS));List<GeoResult> content = nearByXY.getContent();for (GeoResult geoResult : content) {log.info("{}", geoResult.getContent());}GeoResults nearByPlace = getNearByPlace("starbucks", new Distance(120, Metrics.KILOMETERS));content = nearByPlace.getContent();for (GeoResult geoResult : content) {log.info("{}", geoResult.getContent());}getGeoHash("starbucks", "yidiandian", "xicha");del("yidiandian", "xicha");}private void add(String name, Point point) {Long add = redisTemplate.opsForGeo().add(KEY, point, name);log.info("成功添加名称:{} 的坐标信息信息:{}", name, point);}private void get(String... names) {List<Point> position = redisTemplate.opsForGeo().position(KEY, names);log.info("获取名称为:{} 的坐标信息:{}", names, position);}private void del(String... names) {Long remove = redisTemplate.opsForGeo().remove(KEY, names);log.info("删除名称为:{} 的坐标信息数量:{}", names, remove);}/*** 根据坐标 获取指定范围的位置** @param point* @param distance* @return*/private GeoResults getNearByXY(Point point, Distance distance) {Circle circle = new Circle(point, distance);RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance(). // 包含距离includeCoordinates(). // 包含坐标sortAscending(). // 排序 还可选sortDescending()limit(5); // 获取前多少个GeoResults geoResults = redisTemplate.opsForGeo().radius(KEY, circle, args);log.info("根据坐标获取:{} {} 范围的数据:{}", point, distance, geoResults);return geoResults;}/*** 根据一个位置,获取指定范围内的其他位置** @param name* @param distance* @return*/private GeoResults getNearByPlace(String name, Distance distance) {RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands.GeoRadiusCommandArgs.newGeoRadiusArgs().includeDistance(). // 包含距离includeCoordinates(). // 包含坐标sortAscending(). // 排序 还可选sortDescending()limit(5); // 获取前多少个GeoResults geoResults = redisTemplate.opsForGeo().radius(KEY, name, distance, args);log.info("根据位置:{} 获取: {} 范围的数据:{}", name, distance, geoResults);return geoResults;}/*** 获取GEO HASH** @param names* @return*/private List<String> getGeoHash(String... names) {List<String> hash = redisTemplate.opsForGeo().hash(KEY, names);log.info("names:{} 对应的hash:{}", names, hash);return hash;}
}

执行日志:

成功添加名称:starbucks 的坐标信息信息:Point [x=116.624450, y=39.862060]
成功添加名称:yidiandian 的坐标信息信息:Point [x=117.351479, y=38.750125]
成功添加名称:xicha 的坐标信息信息:Point [x=116.538542, y=39.754120]获取名称为:[starbucks, yidiandian, xicha] 的坐标信息:[Point [x=116.624452, y=39.862060], Point [x=117.351480, y=38.750124], Point [x=116.538540, y=39.754119]]根据坐标获取:Point [x=116.000000, y=39.000000] 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 102.8394 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 95.8085 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 109.8703 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])根据位置:starbucks 获取: 120.0 KILOMETERS 范围的数据:GeoResults: [averageDistance: 7.03605 KILOMETERS, results: GeoResult [content: RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060]), distance: 0.0 KILOMETERS, ],GeoResult [content: RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119]), distance: 14.0721 KILOMETERS, ]]
RedisGeoCommands.GeoLocation(name=starbucks, point=Point [x=116.624452, y=39.862060])
RedisGeoCommands.GeoLocation(name=xicha, point=Point [x=116.538540, y=39.754119])names:[starbucks, yidiandian, xicha] 对应的hash:[wx4fvbem6d0, wwgkqqhxzd0, wx4f5vhb8b0]删除名称为:[yidiandian, xicha] 的坐标信息数量:2

相关文章:

活动需求中灵活使用Redis提升生产力

抽奖 一堆用户参与进来&#xff0c;然后随机抽取几个幸运用户给予实物/虚拟的奖品&#xff1b;此时&#xff0c;开发人员就需要写上一个抽奖的算法&#xff0c;来实现幸运用户的抽取&#xff1b;其实我们完全可以利用Redis的集合&#xff08;Set&#xff09;&#xff0c;就能轻…...

Java知识点学习(第16天)

Innodb是如何实现事务的&#xff1f; innodb通过Buffer Pool&#xff0c;LogBuffer&#xff0c;Redo Log&#xff0c;Undo Log来实现事务&#xff0c;以一个update语句为例&#xff1a; innodb在收到一个update语句后&#xff0c;会先根据条件找到数据所在的页&#xff0c;并…...

ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED

昨晚正在外滩玩&#xff0c;有个客户发过来一段报错&#xff0c;已经影响到业务了。一看就是12C以后版本才有的问题&#xff0c;&#xff0c;赶紧在手机中收到临时解决办法 报错如下 ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED partition SYS_P42549 by 1024 in t…...

抖音滑块以及轨迹分析

声明 本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。 本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。 如有侵权,请联系我进行删除。 我们在web端打开用户主页的时候,时不时的会出现滑…...

C#生成单色bmp图片,转为单色bmp图片 任意语言完全用字节拼一张单色图,LCD取模 其它格式图片转为单色图

最终效果&#xff1a; V1.8.2 20230419 文字生成单色BMP图片4.exe 默认1280*720 如果显示不全&#xff0c;请把宽和高加大 字体加大。 首先&#xff0c;用windows画板生成一张1*1白色单色图作为标准&#xff0c;数据如下&#xff1a; 数据解析参考&#xff1a;BMP图像文件完…...

【瑞吉外卖】002 -- 后台登录功能开发

本文章为对 黑马程序员Java项目实战《瑞吉外卖》的学习记录 目录 一、需求分析 1、页面原型展示 2、登录页面展示 3、查看登录请求信息 4、数据模型 二、代码开发 1、创建实体类Employee&#xff0c;和employee表进行映射 2、创建包结构&#xff1a;&#xff08;Controller、Se…...

【电动汽车充电站有序充电调度的分散式优化】基于蒙特卡诺和拉格朗日的电动汽车优化调度(分时电价调度)(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…...

java IO流_1

目录 分类 字节流 InputStream OutputStream 文件拷贝 字符流 FileReader FileWriter 处理流 BufferedReader BufferedWriter 文本拷贝 流是从起源到接受的有序数据&#xff0c;通过流的方式允许程序使用相同的方式来访问不同的输入/输出源。 分类 按数据…...

【回忆 总结】我的大学四年

大学四年关键词速览 如果穿越回大一&#xff0c;你想对大一的你提什么最重要的建议&#xff1f;同样是上网课&#xff0c;我为何能比大多数同学学的更好&#xff1f;回到学校&#xff0c;我的大二似乎一帆风顺&#xff1f;在不断的迷茫和徘徊中&#xff0c;大三的我做出的决定&…...

深度解析OEKO

【深度解析OEKO】 什么是OEKO-TEX Standard 100&#xff1f; OEKO-TEX Standard 100现在是使用最为广泛的纺织品生态标志。OEKO-TEX Standard 100规定的标准是根据最新的科学知识&#xff0c;对纱线、纤维以及各类纺织品的有害物质含量规定限度。只有按照严格检测和检查程序提供…...

Golang gorm

GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly. 一 对多入门 比如要开发cmdb的系统&#xff0c;无论是硬件还是软件。硬件对应的就是对应的哪个开发在用。或者服务对应的是哪个业务模块在使用&#xff0c;或者应用谁在使用。那么这…...

rk3568 适配摄像头 (CIF协议)

rk3568 适配摄像头 (CIF协议) 在RK3568处理器中&#xff0c;支持CIF协议的摄像头可以通过CSI接口连接到处理器&#xff0c;实现视频数据的采集和处理。同时&#xff0c;RK3568还支持多种图像处理算法和编解码器&#xff0c;可以对采集到的视频数据进行实时处理和压缩&#xff…...

今天面试招了个25K的测试员,从腾讯出来的果然都有两把刷子···

公司前段时间缺人&#xff0c;也面了不少测试&#xff0c;前面一开始瞄准的就是中级的水准&#xff0c;也没指望来大牛&#xff0c;提供的薪资在15-25k&#xff0c;面试的人很多&#xff0c;但平均水平很让人失望。看简历很多都是4年工作经验&#xff0c;但面试中&#xff0c;不…...

Redis---集群环境准备

一、redis集群环境准备 1、部署Redis集群的目的&#xff1a; 多台服务器一起提供数据存储服务&#xff1b; 实现数据的分布式存储&#xff1b; 可以实现服务的高可用&#xff1b; 可用实现数据自动备份&#xff1b; 2、服务器IP地址及端口&#xff1a; 主机名 IP地…...

数据结构考研版——队列的配置问题

一、正常配置下的情况 队空状态 frontrear;入队操作 出队操作 队满状态 在正常配置下元素的个数&#xff08;rear>front&#xff09; 当rear<front 综上所述用一个表达式表示&#xff1a;(rear-frontmaxSize)%maxSize 二、非正常配置下的情况1 队空状态 入队操作…...

【SOAP-WebService系列】SOAP学习笔记

目录 1、SOAP是什么&#xff1f; 2、SOAP特性 3、SOAP消息组成 4、SOAP调用 5、SOAP和HTTP 1、SOAP是什么&#xff1f; SOAP(Simple Object Access Protocol&#xff0c;即简单对象访问协议) &#xff0c;是一个轻量级协议&#xff0c;用于在分散的分布式环境中使用XML在对…...

材料科学|名词解释终版!!!

晶体&#xff1a;组成物质的原子&#xff0c;分子或离子按照一定的周期性规则排列形成的固体。 非晶体&#xff1a;原子在三维空间的不规则排列&#xff0c;长程无序&#xff0c;各向同性。 晶体结构&#xff1a;原子&#xff0c;离子&#xff0c;原子团按照空间点阵而进行的…...

永久免费内网穿透不限制速度

市面上的免费内网穿透大都有格式各样的限制&#xff0c;什么限制流量啊&#xff0c;每个月要签到打卡啊&#xff0c;还有更改域名地址等&#xff0c;只有神卓互联内网穿透是永久免费没有限制的&#xff0c;白嫖也可以。 这篇文章分享了3个方案&#xff0c;按照性能和综合指标排…...

JAVA开发运维(云基础设备监控)

在大型的商用系统中&#xff0c;经常需要监控云设备的健康状态&#xff0c;性能情况&#xff0c;流量数据等。及时发现系统问题&#xff0c;及时修复&#xff0c;以确保系统的高可用。检查云资源的工作内容主要包括基础监控、主动拨测、用户体验、APM监控、指标体系、业务分析、…...

现在备考2023年5月软考网络工程师时间够吗?

距离2023年5月软考还有1个多月的时间&#xff0c;备考网络工程师的时间是够的&#xff0c;以下是一些备考方法&#xff1a; 1.了解考试内容 在你开始学习考试之前&#xff0c;了解考试的形式和内容是很重要的。这将帮助你把注意力集中在最有可能被测试的领域。你应该复习考试…...

webp怎么转换成png,4个方法教你快速处理

webp怎么转换成png&#xff1f;目前在一些比较大的图片素材网站下载的图片都是webp格式的。我们都知道webp格式图片&#xff0c;它在正常的图片浏览器中是无法打开的。 所以说我们要把webp图片转变成png格式&#xff0c;正常来说我们常用的图片处理软件也能进行格式转换&#x…...

程序员能干多久?程序员能干到多大年龄?

程序员可以工作多少年?大多数程序员认为程序员是吃青春饭的工作。编程只能干到30岁&#xff0c;最长可达35岁。我经常听到这样的话&#xff0c;都让人倍感压力。今天&#xff0c;我们来谈谈这个老话题...... 程序员能干多久&#xff1f; 根据国外的经验来说&#xff0c;干到…...

采购系统是如何管理供应商的?

随着数字化的推进&#xff0c;企业面临着越来越多的供应商管理问题。企业采购数字化转型已经成为大势所趋&#xff0c;对于采购数字化转型而言&#xff0c;供应商管理是重要一环。 供应商准入管理 在供应商准入阶段&#xff0c;企业需要从供应商资质、财务能力、信誉能力、管理…...

Linux学习笔记(2)--一些内核接口

1&#xff09;dump_stack dump_stack()是Linux内核中的一个函数&#xff0c;用于在内核中输出当前的函数调用栈。该函数会输出当前线程&#xff08;或进程&#xff09;的函数调用栈信息&#xff0c;以及相应的调用地址和虚拟内存地址等信息&#xff0c;一般用于诊断程序运行时…...

学习风`宇博客用户权限菜单模块

文章目录 用户-角色-菜单-资源 各表关系图菜单 和 路由菜单表及分析分析 /api/admin/user/menus接口MenuServiceImpl#listUserMenus接口返回示例及分析 前端代码分析menu.jsSideBar.vue 接口权限控制资源表 及 分析分析 WebSecurityConfig权限控制整体流程先说登录UserDetailsS…...

centos7.6部署ELK集群(一)之elasticsearch7.7.0集群部署

32.3. 部署es7.7.0 32.3.1. 下载es&#xff08;各节点都做&#xff09; wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.7.0-linux-x86_64.tar.gz 32.3.2. 解压至安装目录&#xff08;各节点都做&#xff09; tar -xvf elasticsearch-7.7.0-li…...

leetcode142. 环形链表 II

给定一个链表的头节点 head &#xff0c;返回链表开始入环的第一个节点。 如果链表无环&#xff0c;则返回 null。 如果链表中有某个节点&#xff0c;可以通过连续跟踪 next 指针再次到达&#xff0c;则链表中存在环。 为了表示给定链表中的环&#xff0c;评测系统内部使用整数…...

Linux: network: dummy 类型网络接口

文章目录 参考创建一个重要的用途是在container平台的应用dpdk相关的一个用途另一个用途ChatGPT的回复参考 https://tldp.org/LDP/nag/node72.html 这里举了一个例子,说为什么需要dummy类型的接口:就是一个类local loopback的一个接口,当应用需要给另一个本地的应用发送包的…...

java记录-lambda表达式、接口应用、方法引用

基本形式 (str)->{System.out.println(str) };调用作为参数的接口实例的方法 1、用一个类实现接口&#xff0c;然后使用该类实例调用方法 2、匿名内部类 3、在 接口&#xff08;不能是抽象类&#xff09; 有且只有一个抽象方法时&#xff0c;可以使用lamda表达式来重写这个…...

AI写作机器人-ai文章生成器在线

使用AI续写生成器&#xff0c;让内容创作事半功倍&#xff01; 随着人工智能技术的不断进步和应用&#xff0c;AI续写生成器的出现为内容创作带来了全新的革命。这种技术可以让你的写作事半功倍&#xff0c;让你轻松生成高质量的文章和内容。在这篇文章中&#xff0c;我们将介绍…...