活动需求中灵活使用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提升生产力
抽奖 一堆用户参与进来,然后随机抽取几个幸运用户给予实物/虚拟的奖品;此时,开发人员就需要写上一个抽奖的算法,来实现幸运用户的抽取;其实我们完全可以利用Redis的集合(Set),就能轻…...
Java知识点学习(第16天)
Innodb是如何实现事务的? innodb通过Buffer Pool,LogBuffer,Redo Log,Undo Log来实现事务,以一个update语句为例: innodb在收到一个update语句后,会先根据条件找到数据所在的页,并…...
ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED
昨晚正在外滩玩,有个客户发过来一段报错,已经影响到业务了。一看就是12C以后版本才有的问题,,赶紧在手机中收到临时解决办法 报错如下 ORA-1688: unable to extend table AUDSYS.AUD$UNIFIED partition SYS_P42549 by 1024 in t…...
抖音滑块以及轨迹分析
声明 本文以教学为基准、本文提供的可操作性不得用于任何商业用途和违法违规场景。 本人对任何原因在使用本人中提供的代码和策略时可能对用户自己或他人造成的任何形式的损失和伤害不承担责任。 如有侵权,请联系我进行删除。 我们在web端打开用户主页的时候,时不时的会出现滑…...
C#生成单色bmp图片,转为单色bmp图片 任意语言完全用字节拼一张单色图,LCD取模 其它格式图片转为单色图
最终效果: V1.8.2 20230419 文字生成单色BMP图片4.exe 默认1280*720 如果显示不全,请把宽和高加大 字体加大。 首先,用windows画板生成一张1*1白色单色图作为标准,数据如下: 数据解析参考:BMP图像文件完…...
【瑞吉外卖】002 -- 后台登录功能开发
本文章为对 黑马程序员Java项目实战《瑞吉外卖》的学习记录 目录 一、需求分析 1、页面原型展示 2、登录页面展示 3、查看登录请求信息 4、数据模型 二、代码开发 1、创建实体类Employee,和employee表进行映射 2、创建包结构:(Controller、Se…...
【电动汽车充电站有序充电调度的分散式优化】基于蒙特卡诺和拉格朗日的电动汽车优化调度(分时电价调度)(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
java IO流_1
目录 分类 字节流 InputStream OutputStream 文件拷贝 字符流 FileReader FileWriter 处理流 BufferedReader BufferedWriter 文本拷贝 流是从起源到接受的有序数据,通过流的方式允许程序使用相同的方式来访问不同的输入/输出源。 分类 按数据…...
【回忆 总结】我的大学四年
大学四年关键词速览 如果穿越回大一,你想对大一的你提什么最重要的建议?同样是上网课,我为何能比大多数同学学的更好?回到学校,我的大二似乎一帆风顺?在不断的迷茫和徘徊中,大三的我做出的决定&…...
深度解析OEKO
【深度解析OEKO】 什么是OEKO-TEX Standard 100? OEKO-TEX Standard 100现在是使用最为广泛的纺织品生态标志。OEKO-TEX Standard 100规定的标准是根据最新的科学知识,对纱线、纤维以及各类纺织品的有害物质含量规定限度。只有按照严格检测和检查程序提供…...
Golang gorm
GORM 指南 | GORM - The fantastic ORM library for Golang, aims to be developer friendly. 一 对多入门 比如要开发cmdb的系统,无论是硬件还是软件。硬件对应的就是对应的哪个开发在用。或者服务对应的是哪个业务模块在使用,或者应用谁在使用。那么这…...
rk3568 适配摄像头 (CIF协议)
rk3568 适配摄像头 (CIF协议) 在RK3568处理器中,支持CIF协议的摄像头可以通过CSI接口连接到处理器,实现视频数据的采集和处理。同时,RK3568还支持多种图像处理算法和编解码器,可以对采集到的视频数据进行实时处理和压缩ÿ…...
今天面试招了个25K的测试员,从腾讯出来的果然都有两把刷子···
公司前段时间缺人,也面了不少测试,前面一开始瞄准的就是中级的水准,也没指望来大牛,提供的薪资在15-25k,面试的人很多,但平均水平很让人失望。看简历很多都是4年工作经验,但面试中,不…...
Redis---集群环境准备
一、redis集群环境准备 1、部署Redis集群的目的: 多台服务器一起提供数据存储服务; 实现数据的分布式存储; 可以实现服务的高可用; 可用实现数据自动备份; 2、服务器IP地址及端口: 主机名 IP地…...
数据结构考研版——队列的配置问题
一、正常配置下的情况 队空状态 frontrear;入队操作 出队操作 队满状态 在正常配置下元素的个数(rear>front) 当rear<front 综上所述用一个表达式表示:(rear-frontmaxSize)%maxSize 二、非正常配置下的情况1 队空状态 入队操作…...
【SOAP-WebService系列】SOAP学习笔记
目录 1、SOAP是什么? 2、SOAP特性 3、SOAP消息组成 4、SOAP调用 5、SOAP和HTTP 1、SOAP是什么? SOAP(Simple Object Access Protocol,即简单对象访问协议) ,是一个轻量级协议,用于在分散的分布式环境中使用XML在对…...
材料科学|名词解释终版!!!
晶体:组成物质的原子,分子或离子按照一定的周期性规则排列形成的固体。 非晶体:原子在三维空间的不规则排列,长程无序,各向同性。 晶体结构:原子,离子,原子团按照空间点阵而进行的…...
永久免费内网穿透不限制速度
市面上的免费内网穿透大都有格式各样的限制,什么限制流量啊,每个月要签到打卡啊,还有更改域名地址等,只有神卓互联内网穿透是永久免费没有限制的,白嫖也可以。 这篇文章分享了3个方案,按照性能和综合指标排…...
JAVA开发运维(云基础设备监控)
在大型的商用系统中,经常需要监控云设备的健康状态,性能情况,流量数据等。及时发现系统问题,及时修复,以确保系统的高可用。检查云资源的工作内容主要包括基础监控、主动拨测、用户体验、APM监控、指标体系、业务分析、…...
现在备考2023年5月软考网络工程师时间够吗?
距离2023年5月软考还有1个多月的时间,备考网络工程师的时间是够的,以下是一些备考方法: 1.了解考试内容 在你开始学习考试之前,了解考试的形式和内容是很重要的。这将帮助你把注意力集中在最有可能被测试的领域。你应该复习考试…...
网络编程(Modbus进阶)
思维导图 Modbus RTU(先学一点理论) 概念 Modbus RTU 是工业自动化领域 最广泛应用的串行通信协议,由 Modicon 公司(现施耐德电气)于 1979 年推出。它以 高效率、强健性、易实现的特点成为工业控制系统的通信标准。 包…...
java调用dll出现unsatisfiedLinkError以及JNA和JNI的区别
UnsatisfiedLinkError 在对接硬件设备中,我们会遇到使用 java 调用 dll文件 的情况,此时大概率出现UnsatisfiedLinkError链接错误,原因可能有如下几种 类名错误包名错误方法名参数错误使用 JNI 协议调用,结果 dll 未实现 JNI 协…...
基于服务器使用 apt 安装、配置 Nginx
🧾 一、查看可安装的 Nginx 版本 首先,你可以运行以下命令查看可用版本: apt-cache madison nginx-core输出示例: nginx-core | 1.18.0-6ubuntu14.6 | http://archive.ubuntu.com/ubuntu focal-updates/main amd64 Packages ng…...
Vue2 第一节_Vue2上手_插值表达式{{}}_访问数据和修改数据_Vue开发者工具
文章目录 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染2. 插值表达式{{}}3. 访问数据和修改数据4. vue响应式5. Vue开发者工具--方便调试 1.Vue2上手-如何创建一个Vue实例,进行初始化渲染 准备容器引包创建Vue实例 new Vue()指定配置项 ->渲染数据 准备一个容器,例如: …...
使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台
🎯 使用 Streamlit 构建支持主流大模型与 Ollama 的轻量级统一平台 📌 项目背景 随着大语言模型(LLM)的广泛应用,开发者常面临多个挑战: 各大模型(OpenAI、Claude、Gemini、Ollama)接口风格不统一;缺乏一个统一平台进行模型调用与测试;本地模型 Ollama 的集成与前…...
Linux安全加固:从攻防视角构建系统免疫
Linux安全加固:从攻防视角构建系统免疫 构建坚不可摧的数字堡垒 引言:攻防对抗的新纪元 在日益复杂的网络威胁环境中,Linux系统安全已从被动防御转向主动免疫。2023年全球网络安全报告显示,高级持续性威胁(APT)攻击同比增长65%,平均入侵停留时间缩短至48小时。本章将从…...
链式法则中 复合函数的推导路径 多变量“信息传递路径”
非常好,我们将之前关于偏导数链式法则中不能“约掉”偏导符号的问题,统一使用 二重复合函数: z f ( u ( x , y ) , v ( x , y ) ) \boxed{z f(u(x,y),\ v(x,y))} zf(u(x,y), v(x,y)) 来全面说明。我们会展示其全微分形式(偏导…...
游戏开发中常见的战斗数值英文缩写对照表
游戏开发中常见的战斗数值英文缩写对照表 基础属性(Basic Attributes) 缩写英文全称中文释义常见使用场景HPHit Points / Health Points生命值角色生存状态MPMana Points / Magic Points魔法值技能释放资源SPStamina Points体力值动作消耗资源APAction…...
【Ftrace 专栏】Ftrace 参考博文
ftrace、perf、bcc、bpftrace、ply、simple_perf的使用Ftrace 基本用法Linux 利用 ftrace 分析内核调用如何利用ftrace精确跟踪特定进程调度信息使用 ftrace 进行追踪延迟Linux-培训笔记-ftracehttps://www.kernel.org/doc/html/v4.18/trace/events.htmlhttps://blog.csdn.net/…...
【笔记】AI Agent 项目 SUNA 部署 之 Docker 构建记录
#工作记录 构建过程记录 Microsoft Windows [Version 10.0.27871.1000] (c) Microsoft Corporation. All rights reserved.(suna-py3.12) F:\PythonProjects\suna>python setup.py --admin███████╗██╗ ██╗███╗ ██╗ █████╗ ██╔════╝…...
