mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
docker run -p 6379:6379 --name redis -v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
进入 redis-cli
docker exec -it redis redis-cli
我们输入简单的 get set 指令,查看 redis 是否能成功执行
此时,虽然 redis 是可以运行的,但是数据都没有持久化,一旦重启,数据就消失了,所以需要添加配置
获取配置文件
将信息复制到我们的 redis.conf 文件夹中
找到 appendonly,将后面的 no 改为 yes (即开启 AOP 持久化策略)
我们再进行测试
首先必须将 conf 文件中的 bind 数据进行修改
要将 bind 127.0.0.1 改成 bind 0.0.0.0
(标签:阿里云、腾讯云远程连接 redis失败,RDM)
安装 RDM ,填写 redis 所在的宿主机的 ip username password 即可
基础数据类型的操作,可以参考下面这个链接:https://www.runoob.com/redis/redis-strings.html
这里就不再赘述了
String 使用 get set 进行设置,可以设置定时删除、步长增加,因为 redis 单线程的缘故,所以 incr decr 的操作是原子性的
其底层数据结构很简单,和 java 中的 ArrayList 类似:
如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M
list 底层数据结构是双向链表:
正因如此,list 有左插右插的操作
redis 中的 set 和 java 中的 HashSet 类似,可以自动排重,并提供判断元素是否在 set 中的接口
redis 中的 hash 和 java 中的 map 类似
使用 hash 可能是出于下面这样的考量,比如我们想要存储用户的数据,有下面几个方案:
对象序列化后存储,要使用的时候再提取并反序列化
用冗余的 id+用户信息数据进行存储
这种方案还不如第一种方案
借助 hash 进行存储
通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题
使用 方案3 的话,当我们希望获取用户的某个字段的信息,就不需要像 方案1 那样,将整个对象的信息提取出来再序列化了,提高了信息获取的效率
不过在实际开发中,这种实现方式除非在系统非优化到这步才使用,不然多数情况下为了图方便,我们还是使用 方案1
相较于 set ,zset 为每个数据添加了 score 字段,所有元素会默认根据 score 的大小,从小到大排序
因为这种特性,遇到热榜排行、商城信息排行等业务,使用 zset 就显得分外方便
这里我们模拟一个热销榜:
在像用户展示之前,我们获取的数据顺序如下:
可以看到,排序是按照分数递增的
如果哪件商品的热度下降了,我们可以通过修改其 score 值实现排名的降低:
zset 的底层实现是跳表,跳表简单来说,就是为了解决链表这种数据结构定位慢的问题(O(N)),可以将链表定位的时间复杂度降为 O(logn),其使用的额外空间无限接近与原链表的大小
关于跳表的细节,可以点击下面这个链接去了解:
什么是跳表
订阅者在订阅指定频道后,发布者在对应频道发布信息,所有订阅该频道的 redis 进程都会受到发布者发布的消息
在我们平时的开发过程中,会有一些boolean型数据需要存取,比如用户一年的签到记录,签了是1,没签是0,要记录365天。如果使用普通的key/value,每个用户要记录365个,当用户数上亿的时候,需要的存储空间是惊人的。
为了解决这个问题,redis提供了位图数据结构,这样每天的签到记录只占据一个位,365天就是365个位,46个字节(一个稍长一点的字符串)就可以完全容纳下,这就大大节约了存储空间。位图的最小单位是比特(bit),每个bit的取值只能是0或1。
这里我们设置一个场景,使用 Bitmap 记录一款应用当天的用户登录情况
假设 2022年3月22日,1、6、11、15、19号用户登录了应用
这里我们要注意,有些情况下,为用户设置的 id 头几位可能有重复数,比如 1号用户为 100001,2号用户为100002,如果直接将用户 id 作为参数存入 bitmap 中,那么 bitmap 的偏移量会很大,导致初始化时间过长,进而使得 redis 产生阻塞
对于这种情况,我们应该在将数据存入 bitmap 之前,去除所有数字头部重复的部分
为了了解某个用户在某天有无登录,可以使用 gitbit 获取bitmap 数据
1号用户在 2022年3月22日进行了登录操作:
为了统计某天的用户月活,我们可以使用 bitcount 统计 bitmap 中 1 的个数
2022年3月22日日活人数为 5:
bitop 可以对两个 bitmap 进行与、或、非、异或操作,并将操作结果存入一个新的 bitmap 中
统计 20220327 和 20220328 都在线的人数:
对于这个请求,我们可以对这两天的 bitmap 进行与操作
结果存入新的 bitmap users:and:20220327_28
中
统计两日都在线人数,只需要对新的 bitmap 进行 bitcount 操作即可
除了上面介绍的统计日活月活外,涉及到大量用户、长时间统计的业务,都可以考虑使用 bitmap
比如签到业务,使用 uid 做 key,单个 bitmap 统计用户从注册开始的签到状态。这样,在3亿用户的前提下,一年的数据使用量是
300000000*365/8/1024/1024/1024 = 12.74 GB
,还是很划算的
乍看之下,好像使用 bitmap 必定会比 set 节省空间
我们以统计日活做例子
那么单日空间消耗为:
bitmap 占用空间:1 0000 0000 /8/1024/1024/1024 = 0.01 GB
set 占用空间:5000 0000 * 64 /8 /1024/1024/1024 = 0.32GB
一年下来空间消耗为:
Bitmap: 3.65GB
Set : 116.8 GB
可以发现,bitmap 节省的空间还是十分客观的
但是,假设网站虽然有很高的注册量,但是有大量僵尸粉,情况就大不相同了
单日空间消耗:
bitmap 占用空间:1 0000 0000 /8/1024/1024/1024 = 0.01 GB
set 占用空间:10 0000 * 64 /8 /1024/1024/1024 = 0.0007GB
一年下来的空间消耗:
Bitmap: 3.65GB
Set : 0.26 GB
所以说,是否使用 Bitmap,还是要依照业务量来的
实际业务中,我们有统计 PV(PageView)即页面观看数的需求,方便后续对业务做出调整,这个功能我们可以很方便的使用 redis 的 incr,incrby 统计
但是,像 UV(unique visiter) 即独立访客,在统计访问量的基础上,还需要进行去重,这时候单纯的使用 incr ,incrby 就不好使了,对于这样的业务需求,可以归于一种基数统计需求。所谓基数统计,即假设一个数据集 {1,1,2,2,3},则其基数集是其去重后的集合,即{1,2,3},则其基数统计的结果为 3
对于这个问题,解决方案有很多:
(1)使用 mysql 存储单个 ip 的访问记录
(2)使用 set bitmap 这些数据结构进行存储
但是对于亿级的用户,方案**(1)**自不必说,浪费大量不必要的空间的同时,数据库还有很大的概率寄,为了这么小一个功能耗费这么多资源显然不值当
方案 (2) 中的 set 因为占用量大也不合适,而 bitmap 虽然对空间已经做到尽可能压缩了,但是相对于功能的重要程度,其占用的空间还是大了一些,bitmap 长度为 2^32 ,对于类似 google 搜索页面要统计每日访问的独立 IP的数量,单就 几十亿级别的搜索次数,少说每日资源开销就是几百 mb,累计下来资源消耗还是蛮哈人的
其实对于十几亿级的统计需求,几十亿和几十亿零几百万的区别是不大的,这个时候,我们就需要一种精度可以不是很高,但是空间消耗一定要小的算法,HyperLogLog 边应运而生
在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。
向 hll 中"添加"元素,并使用指定算法去重统计元素个数
返回基数
pfcount 除了统计单个 hll 外,也可以同时统计多个 hll
假设我们想要统计两日的 UV 数据:
day1 访问基数集合为 {ip1,ip2,ip4},day2 访问基数集合为{ip1,ip5}
所以两日访问基数集合为{ip1,ip2,ip4,ip5},即访问基数为 4
可以将一个或多个 hll 合并后,并入一个新的 hll中
比如我们只记录了单日 uv,想获取 当月uv 的时候,只需要对当月所有当日 uv 进行 merge 操作即可
这里要注意,HyperLogLog 只能统计基数,但是无法返回基数对应的元素
而且其计算精度不高,在统计数据量不大的前提下,还是建议使用 set 或者 bitmap
导入 jedis 依赖:
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
<version>2.9.0version>
dependency>
编写测试函数:
redis 有 ping pong 机制,如果连接成功,我们在 ping redis 的时候,会返回给我们一个 pong
public class JedisDemo1 {
public static void main(String[] args) {
Jedis jedis = new Jedis("" );
jedis.auth("<如果 redis 有设置密码,这里要填写>");
System.out.println(jedis.ping());
jedis.close();
}
}
测试:
注意: 如果返回异常,我们可以排查下面几个点
1、宿主机 ip 是否填写正确
2、redis 有没有设置密码,密码有没有输对
3、检查自己的网是否正常
基本操作示例:
jedis 的操作和 redis-cli 中的操作基本是一致的
熟悉了 redis-cli 的操作,jedis 可以快速上手
Jedis jedis = JedisUtil.getInstance();
jedis.set("key3","key3Val");
System.out.println(jedis.get("key3")); //key3Val
Jedis jedis = JedisUtil.getInstance();
jedis.lpush("list3","4","1","5","4","1","1");
System.out.println(jedis.lrange("list3",0,-1)); //[1, 1, 4, 5, 1, 4]
jedis.sadd("set3","redis","redis","mysql");
Set<String> set3 = jedis.smembers("set3");
System.out.println(set3);
要求:
1、输入手机号,点击发送六位验证码,有效期2 min
2、点击验证,返回成功或者失败
3、每个手机每天只能请求3次验证码
主要功能函数如下:
/**
* 验证验证码是否有效
* @param phoneNum
* @param verifyCode
* @return
*/
public static String verifyCode(String phoneNum,String verifyCode) {
Jedis jedis = JedisUtil.getInstance();
String verifyCodeKey = "verify:code:" + phoneNum;
String currVerifyCode = jedis.get(verifyCodeKey);
if (currVerifyCode==null) {
return "对不起,验证超时,请重新获取";
}
if (!currVerifyCode.equals(verifyCode)) {
return "输入错误,请重新输入";
}
return "验证成功";
}
/**
* 根据手机号获取验证码
* 1、同一手机一天发送从超过三次返回:"对不起,当天发送超出超过3次,请隔天再试"
* 2、每个验证码有效期为 60s
* @param phoneNum
* @return
*/
public static String getVerifyCode(String phoneNum) {
Jedis jedis = JedisUtil.getInstance();
// 验证次数
String verifyTimesKey = "verify:times:" + phoneNum;
// 缓存中的
String verifyCodeKey = "verify:code:" + phoneNum;
String times = jedis.get(verifyTimesKey);
// 未发送过验证码或者发送次数没有超过三次
if (times==null || Integer.parseInt(times)<=2) {
if (times==null) {
times = "0";
}
jedis.set(verifyTimesKey,String.valueOf(Integer.parseInt(times)+1));
String verifyCode = getRandomCode(6);
jedis.setex(verifyCodeKey,60,verifyCode);
return verifyCode;
} else {
jedis.setex(verifyTimesKey,60*60*24,"4");
}
return "错误,当日请求超过3次";
}
/**
* 获取随机指定位验证码
* @return
*/
public static String getRandomCode(int num) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < num; i++) {
builder.append(random.nextInt(10));
}
return builder.toString();
}
我们再写个主函数,方便测试:
public static void main(String[] args) {
while (true) {
System.out.println("===========================");
System.out.println("1:请求");
System.out.println("2:验证");
String input = sc.next();
if (!"1".equals(input) && !"2".equals(input)) {
System.out.println("输入错误");
continue;
}
if ("1".equals(input)) {
System.out.println("请输入电话号码:");
String phoneNum = sc.next();
System.out.println("请求结果为:");
System.out.println(getVerifyCode(phoneNum));
} else {
System.out.println("请输入电话:");
String phoneNum = sc.next();
System.out.println("请输入验证码:");
String verifyCode = sc.next();
System.out.println("验证结果为:");
System.out.println(verifyCode(phoneNum,verifyCode));
}
}
}
可以发现,请求、验证、次数限制、超时都正常工作:
创建springboot项目,引入 redis 和连接池依赖:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
dependency>
编写配置文件:
spring:
redis:
host: <宿主机 ip>
port: 6379
database: 0 # 默认选择的数据库
timeout: 180000 # 超时放弃时间
password: >
lettuce:
pool:
max-active: 20 # 最大连接数
max-wait: -1 # 最大阻塞等待时间(负数表示没限制)
max-idle: 5 # 最大空闲数
min-idle: 0 # 最小空闲连接
配置类:
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setConnectionFactory(factory);
//key序列化方式
template.setKeySerializer(redisSerializer);
//value序列化
template.setValueSerializer(jackson2JsonRedisSerializer);
//value hashmap序列化
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600))
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
}
测试接口:
@RestController
@RequestMapping("/redis")
public class RedisTestController {
@Autowired
private RedisTemplate redisTemplate;
@RequestMapping("/name")
public String redisName() {
redisTemplate.opsForValue().set("redisName","My name is redis");
return (String) redisTemplate.opsForValue().get("redisName");
}
}
测试:
本篇文章主要介绍了 redis 的一些基础操作,并借助这些基础操作,完成了一个验证码的功能
下一篇,将介绍 redis 分布式锁、秒杀、集群相关的知识,敬请期待