redis 缓存什么数据(Redis只会用缓存20种妙用让同事直呼惊艳)
redis 缓存什么数据(Redis只会用缓存20种妙用让同事直呼惊艳)连续签到的判断依据就是:从右往左计算连续为1的BIT个数,二进制 1011 表示连续签到的天数就是2天,2天的计算过程如下:上面测试了1-4号的签到情况,通过BITFIELD获取出来signFlag = 11(十进制) = 1011(二进制);需求:假如当前为8月4号,检测本月的签到情况,用户分别于1、3、4号签到过# 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
一个月的签到情况,4个字节就记录了(图源:网络)
如上图所示,将一个月的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(十进制)
是否签到、连续签到判断签到功能中,最不好理解的就是是否签到、连续签到的判断,在下面SpringBoot代码中,就是通过这样的:signFlag >> 1 << 1 != signFlag来判断的,稍微有一点不好理解,在这里提前讲述一下;
上面测试了1-4号的签到情况,通过BITFIELD获取出来signFlag = 11(十进制) = 1011(二进制);
连续签到的判断依据就是:从右往左计算连续为1的BIT个数,二进制 1011 表示连续签到的天数就是2天,2天的计算过程如下:
- 第一步,获取signFlag
- 第二步,循环天数,以上测试用例是4天的签到情况,for循环也就是4次
- 第三步,从右往左循环判断连续签到:遇到第一个false的时候,终止并得到连续天数签到详情:循环所有天数,true就表示当前签到了,false表示当天未签到;第一次循环signFlag = 1011
signFlag >> 1 结果: 101
signFlag << 1 结果:1010
1010 != signFlag(1011) 结果:true //4号已签到,说明连续签到1天
signFlag >>= 1 结果: 101 // 此时signFlag = 101
第二次循环signFlag = 101 // 前一次循环计算的结果
signFlag >> 1 结果: 10
signFlag << 1 结果:100
100 != signFlag(101) 结果:true //3号已签到,说明连续签到2天
signFlag >>= 1 结果: 10 // 此时signFlag = 10
第三次循环signFlag = 10 // 前一次循环计算的结果
signFlag >> 1 结果: 1
signFlag << 1 结果:10
10 != signFlag(10) 结果:false //2号未签到,说明连续签到从这里就断了
signFlag >>= 1 结果: 1 // 此时signFlag = 1
到这一步,遇到第一个false,说明连续签到中断;第四次循环:signFlag = 1 // 前一次循环计算的结果
signFlag >> 1 结果: 0
signFlag << 1 结果: 0
0 != signFlag(1) 结果:true //1号已签到
到此,根据BITFIELD获取出来11(十进制),就能得到1、3、4号已签到,2号未签到;连续签到2天;
理解上面的逻辑之后,再来看下面的SpringBoot代码,就会容易很多了;
SpringBoot实现签到签到的方式一般就两种,按月(周)/ 自定义周期,下面将两种方式的签到全部列举出来,以供大家参考:
按月签到签到工具类:
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 一行Java
* @title: 按月签到
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/7/18 18:28
*/
@Slf4j
@Service
public class SignByMonthServiceImpl {
@Autowired
StringRedisTemplate 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;
}
}
测试类:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.Map;
/**
* @author 一行Java
* @title: SignTest2
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/7/19 12:06
*/
@SpringBootTest
@Slf4j
public class SignTest2 {
@Autowired
private SignByMonthServiceImpl signByMonthService;
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 测试用户按月签到
*/
@Test
public 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) "天");
}
}
执行日志:
c.e.r.bitmap_sign_by_month.SignTest2 : 560用户今日是否已签到:0
c.e.r.bitmap_sign_by_month.SignTest2 : 本月签到情况:
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-12: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-11: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-10: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-31: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-30: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-19: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-18: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-17: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-16: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-15: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-14: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-13: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-23: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-01: √
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-22: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-21: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-20: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-09: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-08: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-29: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-07: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-28: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-06: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-27: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-05: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-26: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-04: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-25: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-03: √
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-24: -
c.e.r.bitmap_sign_by_month.SignTest2 : 2022-08-02: -
c.e.r.bitmap_sign_by_month.SignTest2 : 本月一共签到:2天
c.e.r.bitmap_sign_by_month.SignTest2 : 目前连续签到:1天
指定时间签到
签到工具类:
package com.ehang.redis.bitmap_sign_by_range;
import lombok.extern.slf4j.Slf4j;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.connection.BitFieldSubCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author 一行Java
* @title: SignByRangeServiceImpl
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/7/19 12:27
*/
@Slf4j
@Service
public class SignByRangeServiceImpl {
@Autowired
StringRedisTemplate 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;
}
}
测试工具类:
package com.ehang.redis.bitmap_sign_by_range;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Map;
/**
* @author 一行Java
* @title: SignTest
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/7/18 16:11
*/
@SpringBootTest
@Slf4j
public class SignTest {
@Autowired
SignByRangeServiceImpl signByRangeService;
@Test
void 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() ? "√" : "-"));
}
}
}
输出日志:
签到开始时间: 2022-08-01T01:00:00
活动ID: 1 用户ID: 8899
今日是否签到: √
签到操作之前的签到状态:√ (-:表示今日第一次签到,√:表示今天已经签到过了)
总共签到: 3 天
连续签到: 2 天
签到详情> 2022-08-01 : √
签到详情> 2022-08-04 : √
签到详情> 2022-08-03 : √
签到详情> 2022-08-02 : -
9GEO搜附近
很多生活类的APP都具备一个搜索附近的功能,比如美团搜索附近的商家;
网图
如果自己想要根据经纬度来实现一个搜索附近的功能,是非常麻烦的;但是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:计算两个位置之间的距离。单位参数: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" - m :米,默认单位。
- km :千米。
- mi :英里。
- ft :英尺。
- georadius:根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。参数说明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) 4069151800882301
4) 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" - m :米,默认单位。
- km :千米。
- mi :英里。
- ft :英尺。
- WITHDIST: 在返回位置元素的同时, 将位置元素与中心之间的距离也一并返回。
- WITHCOORD: 将位置元素的经度和纬度也一并返回。
- WITHHASH: 以 52 位有符号整数的形式, 返回位置元素经过原始 geohash 编码的有序集合分值。 这个选项主要用于底层应用或者调试, 实际中的作用并不大。
- COUNT 限定返回的记录数。
- ASC: 查找结果根据距离从近到远排序。
- DESC: 查找结果根据从远到近排序。
- georadiusbymember:根据储存在位置集合里面的某个地点获取指定范围内的地理位置集合。功能和上面的georadius类似,只是georadius是以经纬度坐标为中心,这个是以某个地点为中心;
- geohash:返回一个或多个位置对象的 geohash 值。127.0.0.1:6379> GEOHASH drinks starbucks xicha
1) "wx4fvbem6d0"
2) "wx4f5vhb8b0"
通过SpringBoot操作GEO的示例如下
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.List;
/**
* @author 一行Java
* @title: GEOTest
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/7/28 17:29
*/
@SpringBootTest
@Slf4j
public class GEOTest {
private final String KEY = "geo:drinks";
@Autowired
RedisTemplate redisTemplate;
@Test
public 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
10简单限流
为了保证项目的安全稳定运行,防止被恶意的用户或者异常的流量打垮整个系统,一般都会加上限流,比如常见的sential、hystrix,都是实现限流控制;如果项目用到了Redis,也可以利用Redis,来实现一个简单的限流功能;
功能所需命令
- INCR:将 key 中储存的数字值增一
- Expire:设置key的有效期
127.0.0.1:6379> INCR r:f:user1
(integer) 1
# 第一次 设置一个过期时间
127.0.0.1:6379> EXPIRE r:f:user1 5
(integer) 1
127.0.0.1:6379> INCR r:f:user1
(integer) 2
# 等待5s 再次增加 发现已经重置了
127.0.0.1:6379> INCR r:f:user1
(integer) 1
SpringBoot示例:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author 一行Java
* @title: 基于Redis的简单限流
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/8/2 9:43
*/
@SpringBootTest
@Slf4j
public class FreqTest {
// 单位时间(秒)
private static final Integer TIME = 5;
// 允许访问上限次数
private static final Integer MAX = 100;
@Autowired
RedisTemplate redisTemplate;
@Test
public void test() throws Exception {
String userName = "user1";
int tag = 1;
boolean frequency = frequency(userName);
log.info("第{}次是否放行:{}" tag frequency);
for (int i = 0; i < 100; i ) {
tag = 1;
frequency(userName);
}
frequency = frequency(userName);
log.info("第{}次是否放行:{}" tag frequency);
Thread.sleep(5000);
frequency = frequency(userName);
log.info("模拟等待5s后,第{}次是否放行:{}" tag frequency);
}
/**
* 校验访问频率
*
* @param uniqueId 用于限流的唯一ID 可以是用户ID、或者客户端IP等
* @return true:放行 false:拦截
*/
private boolean frequency(String uniqueId) {
String key = "r:q:" uniqueId;
Long increment = redisTemplate.opsForValue().increment(key);
if (increment == 1) {
redisTemplate.expire(key TIME TimeUnit.SECONDS);
}
if (increment <= MAX) {
return true;
}
return false;
}
}
运行日志:
user1 第1次请求是否放行:true
user1 第101次请求是否放行:false
模拟等待5s后,user1 第101次请求是否放行:true
11全局ID
在分布式系统中,很多场景下需要全局的唯一ID,由于Redis是独立于业务服务的其他应用,就可以利用Incr的原子性操作来生成全局的唯一递增ID
功能所需命令
- INCR:将 key 中储存的数字值增一
127.0.0.1:6379> incr g:uid
(integer) 1
127.0.0.1:6379> incr g:uid
(integer) 2
127.0.0.1:6379> incr g:uid
(integer) 3
12简单分布式锁
在分布式系统中,很多操作是需要用到分布式锁,防止并发操作带来一些问题;因为redis是独立于分布式系统外的其他服务,因此就可以利用redis,来实现一个简单的不完美分布式锁;
功能所需命令
- SETNX key不存在,设置;key存在,不设置# 加锁
127.0.0.1:6379> SETNX try_lock 1
(integer) 1
# 释放锁
127.0.0.1:6379> del try_lock
(integer) 1 - set key value [ex seconds] [nx | xx]上面的方式,虽然能够加锁,但是不难发现,很容易出现死锁的情况;比如,a用户在加锁之后,突然系统挂了,此时a就永远不会释放他持有的锁了,从而导致死锁;为此,我们可以利用redis的过期时间来防止死锁问题set try_lock 1 ex 5 nx
不完美的锁
上面的方案,虽然解决了死锁的问题,但是又带来了一个新的问题,执行时间如果长于自动释放的时间(比如自动释放是5秒,但是业务执行耗时了8秒),那么在第5秒的时候,锁就自动释放了,此时其他的线程就能正常拿到锁,简单流程如下:
此时相同颜色部分的时间区间是由多线程同时在执行。而且此问题在此方案下并没有完美的解决方案,只能做到尽可能的避免:
- 方式一,value设置为随机数(如:1234),在程序释放锁的时候,检测一下是不是自己加的锁;比如,A线程在第8s释放的锁就是线程B加的,此时在释放的时候,就可以检验一下value是不是自己当初设置的值(1234),是的就释放,不是的就不管了;
- 方式二,只在时间消耗比较小的业务上选用此方案,尽可能的避免执行时间超过锁的自动释放时间
在支付宝、抖音、QQ等应用中,都会看到好友推荐;
好友推荐往往都是基于你的好友关系网来推荐,将你可能认识的人推荐给你,让你去添加好友,如果随意在系统找个人推荐给你,那你认识的可能性是非常小的,此时就失去了推荐的目的;
比如,A和B是好友,B和C是好友,此时A和C认识的概率是比较大的,就可以在A和C之间的好友推荐;
基于这个逻辑,就可以利用 Redis 的 Set 集合,缓存各个用户的好友列表,然后以差集的方式,来实现好友推荐;
功能所需的命令
- SADD key member [member …]:集合中添加元素,缓存好友列表
- SDIFF key [key …]:取两个集合间的差集,找出可以推荐的用户
# 记录 用户1 的好友列表
127.0.0.1:6379> SADD fl:user1 user2 user3
(integer) 2
# 记录 用户2 的好友列表
127.0.0.1:6379> SADD fl:user2 user1 user4
(integer) 2
# 用户1 可能认识的人 ,把自己(user1)去掉,user4是可能认识的人
127.0.0.1:6379> SDIFF fl:user2 fl:user1
1) "user1"
2) "user4"
# 用户2 可能认识的人 ,把自己(user2)去掉,user3是可能认识的人
127.0.0.1:6379> SDIFF fl:user1 fl:user2
1) "user3"
2) "user2"
不过这只是推荐机制中的一种因素,可以借助其他条件,来增强推荐的准确度;
14发布/订阅发布/订阅是比较常用的一种模式;在分布式系统中,如果需要实时感知到一些变化,比如:某些配置发生变化需要实时同步,就可以用到发布,订阅功能
常用API
- PUBLISH channel message:将消息推送到指定的频道
- SUBSCRIBE channel [channel …]:订阅给定的一个或多个频道的信息
如上图所示,左侧多个客户端订阅了频道,当右侧客户端往频道发送消息的时候,左侧客户端都能收到对应的消息。
15消息队列说到消息队列,常用的就是Kafka、RabbitMQ等等,其实 Redis 利用 List 也能实现一个消息队列;
功能所需的指令
- RPUSH key value1 [value2]:在列表中添加一个或多个值;
- BLPOP key1 [key2] timeout:移出并获取列表的第一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止;
- BRPOP key1 [key2] timeout:移出并获取列表的最后一个元素, 如果列表没有元素会阻塞列表直到等待超时或发现可弹出元素为止。
依赖调整:
Spring Boot 从 2.0版本开始,将默认的Redis客户端Jedis替换为Lettuce,在测试这块阻塞的时候,会出现一个超时的异常io.lettuce.core.RedisCommandTimeoutException: Command timed out after 1 minute(s);没有找到一个好的解决方式,所以这里将 Lettuce 换回成 Jedis ,就没有问题了,pom.xml 的配置如下:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</exclusion>
<exclusion>
<artifactId>lettuce-core</artifactId>
<groupId>io.lettuce</groupId>
</exclusion>
</exclusions>
</dependency>
<!-- jedis客户端 -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2,使用jedis必须依赖它-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
测试代码:
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;
import java.util.concurrent.TimeUnit;
/**
* @author 一行Java
* @title: QueueTest
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/8/5 14:27
*/
@SpringBootTest
@Slf4j
public class QueueTest {
private static final String REDIS_LP_QUEUE = "redis:lp:queue";
private static final String REDIS_RP_QUEUE = "redis:rp:queue";
@Autowired
StringRedisTemplate stringRedisTemplate;
/**
* 先进后出队列
*/
@Test
public void rightMonitor() {
while (true) {
Object o = stringRedisTemplate.opsForList().rightPop(REDIS_LP_QUEUE 0 TimeUnit.SECONDS);
log.info("先进后出队列 接收到数据:{}" o);
}
}
/**
* 先进先出队列
*/
@Test
public void leftMonitor() {
while (true) {
Object o = stringRedisTemplate.opsForList().leftPop(REDIS_RP_QUEUE 0 TimeUnit.SECONDS);
log.info("先进先出队列 接收到数据:{}" o);
}
}
}
- 先进先出测试效果
- 先进后出测试效果
不过,对消息的可靠性要求比较高的场景,建议还是使用专业的消息队列框架,当值被弹出之后,List 中就已经不存在对应的值了,假如此时程序崩溃,就会出现消息的丢失,无法保证可靠性;虽然说也有策略能够保证消息的可靠性,比如,在弹出的同时,将其保存到另外一个队列(BRPOPLPUSH),成功之后,再从另外的队列中移除,当消息处理失败或者异常,再重新进入队列执行,只是这样做就得不偿失了。
16数据共享(session共享)既然Redis能持久化数据,就可以用它来实现模块间的数据共享;SpringBoot Session 利用的这个机制来实现 Session 共享;
- 依赖<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency> - 开启session共享@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
} - 测试代码package com.ehang.redis.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* @author session 共享
* @title: RedisSessionController
* @projectName ehang-spring-boot
* @description: TODO
* @date 2022/8/5 15:58
*/
@RestController
@RequestMapping("session")
public class RedisSessionController {
/**
* 设置session的值
* @param request
* @return
*/
@GetMapping("set")
public Map set(HttpServletRequest request) {
String id = request.getSession().getId();
Map<String String> vas = new HashMap<>();
String key = "key";
String value = "value";
vas.put("id" id);
vas.put(key value);
// 自定义session的值
request.getSession().setAttribute(key value);
return vas;
}
/**
* 获取session的值
* @param request
* @return
*/
@GetMapping("get")
public Map get(HttpServletRequest request) {
Map<String Object> vas = new HashMap<>();
// 遍历所有的session值
Enumeration<String> attributeNames = request.getSession().getAttributeNames();
while (attributeNames.hasMoreElements()) {
String k = attributeNames.nextElement();
Object va = request.getSession().getAttribute(k);
vas.put(k va);
}
vas.put("id" request.getSession().getId());
return vas;
}
} - 测试开启两个服务,分别接听8080和8081,8080调用赋值接口,8081调用获取接口,如下图,可以看到,两个服务共享了一份Session数据;
- Redis中保存的数据127.0.0.1:6379> keys spring:*
1) "spring:session:sessions:expires:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a"
2) "spring:session:expirations:1659688680000"
3) "spring:session:sessions:6f1d7d53-fe01-4e80-9e6a-5ff54fffa92a"
商城类的应用,都会有类似于下图的一个商品筛选的功能,来帮用户快速搜索理想的商品;