Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV

Redis 基础入门

一、安装

  • 创建挂载文件
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
  • 启动 redis 容器
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 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第1张图片

此时,虽然 redis 是可以运行的,但是数据都没有持久化,一旦重启,数据就消失了,所以需要添加配置

  • 进入下面的网址,添加 redis 的自描述文件

获取配置文件

将信息复制到我们的 redis.conf 文件夹中

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第2张图片

找到 appendonly,将后面的 no 改为 yes (即开启 AOP 持久化策略)

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第3张图片

我们再进行测试

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第4张图片

  • 安装可视化管理程序

首先必须将 conf 文件中的 bind 数据进行修改

要将 bind 127.0.0.1 改成 bind 0.0.0.0

(标签:阿里云、腾讯云远程连接 redis失败,RDM)

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第5张图片

安装 RDM ,填写 redis 所在的宿主机的 ip username password 即可

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第6张图片



二、基础数据类型

基础数据类型的操作,可以参考下面这个链接:https://www.runoob.com/redis/redis-strings.html

这里就不再赘述了


1、String

String 使用 get set 进行设置,可以设置定时删除、步长增加,因为 redis 单线程的缘故,所以 incr decr 的操作是原子性的

其底层数据结构很简单,和 java 中的 ArrayList 类似:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第7张图片

如图中所示,内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次只会多扩1M的空间。需要注意的是字符串最大长度为512M


2、list

list 底层数据结构是双向链表:

双向链表

正因如此,list 有左插右插的操作


3、set

redis 中的 set 和 java 中的 HashSet 类似,可以自动排重,并提供判断元素是否在 set 中的接口


4、hash

redis 中的 hash 和 java 中的 map 类似

使用 hash 可能是出于下面这样的考量,比如我们想要存储用户的数据,有下面几个方案:

  • 方案1:

对象序列化后存储,要使用的时候再提取并反序列化

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第8张图片

  • 方案2:

用冗余的 id+用户信息数据进行存储

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第9张图片

这种方案还不如第一种方案

  • 方案3:

借助 hash 进行存储

通过 key(用户ID) + field(属性标签) 就可以操作对应属性数据了,既不需要重复存储数据,也不会带来序列化和并发修改控制的问题

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第10张图片

使用 方案3 的话,当我们希望获取用户的某个字段的信息,就不需要像 方案1 那样,将整个对象的信息提取出来再序列化了,提高了信息获取的效率

不过在实际开发中,这种实现方式除非在系统非优化到这步才使用,不然多数情况下为了图方便,我们还是使用 方案1


5、zset

相较于 set ,zset 为每个数据添加了 score 字段,所有元素会默认根据 score 的大小,从小到大排序

因为这种特性,遇到热榜排行、商城信息排行等业务,使用 zset 就显得分外方便

这里我们模拟一个热销榜:

热销榜

在像用户展示之前,我们获取的数据顺序如下:

可以看到,排序是按照分数递增的

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第11张图片

如果哪件商品的热度下降了,我们可以通过修改其 score 值实现排名的降低:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第12张图片

zset 的底层实现是跳表,跳表简单来说,就是为了解决链表这种数据结构定位慢的问题(O(N)),可以将链表定位的时间复杂度降为 O(logn),其使用的额外空间无限接近与原链表的大小

关于跳表的细节,可以点击下面这个链接去了解:

什么是跳表



三、发布/订阅模式

订阅者在订阅指定频道后,发布者在对应频道发布信息,所有订阅该频道的 redis 进程都会受到发布者发布的消息

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第13张图片

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第14张图片

  • 打开一个客户端,订阅 channel1:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第15张图片

  • 打开另一个客户端,作为发布者向 channel1 中发布信息 hello:

发布者

  • 可以看到,订阅者收到了信息:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第16张图片



四、Redis 新数据类型


1、Bitmap

在我们平时的开发过程中,会有一些boolean型数据需要存取,比如用户一年的签到记录,签了是1,没签是0,要记录365天。如果使用普通的key/value,每个用户要记录365个,当用户数上亿的时候,需要的存储空间是惊人的。

为了解决这个问题,redis提供了位图数据结构,这样每天的签到记录只占据一个位,365天就是365个位,46个字节(一个稍长一点的字符串)就可以完全容纳下,这就大大节约了存储空间。位图的最小单位是比特(bit),每个bit的取值只能是0或1。

1)使用

这里我们设置一个场景,使用 Bitmap 记录一款应用当天的用户登录情况

  • setbit:

假设 2022年3月22日,1、6、11、15、19号用户登录了应用

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第17张图片

这里我们要注意,有些情况下,为用户设置的 id 头几位可能有重复数,比如 1号用户为 100001,2号用户为100002,如果直接将用户 id 作为参数存入 bitmap 中,那么 bitmap 的偏移量会很大,导致初始化时间过长,进而使得 redis 产生阻塞

对于这种情况,我们应该在将数据存入 bitmap 之前,去除所有数字头部重复的部分

  • getbit:

为了了解某个用户在某天有无登录,可以使用 gitbit 获取bitmap 数据

1号用户在 2022年3月22日进行了登录操作:

getbit

  • bitcount:

为了统计某天的用户月活,我们可以使用 bitcount 统计 bitmap 中 1 的个数

2022年3月22日日活人数为 5:

bitcount

  • bitop:

bitop 可以对两个 bitmap 进行与、或、非、异或操作,并将操作结果存入一个新的 bitmap 中

统计 20220327 和 20220328 都在线的人数:

对于这个请求,我们可以对这两天的 bitmap 进行操作

结果存入新的 bitmap users:and:20220327_28

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第18张图片

统计两日都在线人数,只需要对新的 bitmap 进行 bitcount 操作即可

统计两日都在线的人数


2)实际应用场景

除了上面介绍的统计日活月活外,涉及到大量用户、长时间统计的业务,都可以考虑使用 bitmap

比如签到业务,使用 uid 做 key,单个 bitmap 统计用户从注册开始的签到状态。这样,在3亿用户的前提下,一年的数据使用量是

300000000*365/8/1024/1024/1024 = 12.74 GB,还是很划算的


3)bitmap 与 set 比较

乍看之下,好像使用 bitmap 必定会比 set 节省空间

我们以统计日活做例子

  • 假设网站注册用户数为 1亿,日活 5千万,单个用户 id 占据 64bit

那么单日空间消耗为:

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 节省的空间还是十分客观的

但是,假设网站虽然有很高的注册量,但是有大量僵尸粉,情况就大不相同了

  • 假设网站还是 1亿注册量,但是日活只有 10 万

单日空间消耗:

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,还是要依照业务量来的



2、hyperLogLog

1)介绍

实际业务中,我们有统计 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 个不同元素的基数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。


2)使用

  • pfadd:

向 hll 中"添加"元素,并使用指定算法去重统计元素个数

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第19张图片

  • pfcount

返回基数

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第20张图片

pfcount 除了统计单个 hll 外,也可以同时统计多个 hll

假设我们想要统计两日的 UV 数据:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第21张图片

day1 访问基数集合为 {ip1,ip2,ip4},day2 访问基数集合为{ip1,ip5}

所以两日访问基数集合为{ip1,ip2,ip4,ip5},即访问基数为 4


  • pfmerge:

可以将一个或多个 hll 合并后,并入一个新的 hll中

比如我们只记录了单日 uv,想获取 当月uv 的时候,只需要对当月所有当日 uv 进行 merge 操作即可



这里要注意,HyperLogLog 只能统计基数,但是无法返回基数对应的元素

而且其计算精度不高,在统计数据量不大的前提下,还是建议使用 set 或者 bitmap



五、Redis_Jedis 测试

1、Jedis 入门

导入 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();
    }
}

测试:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第22张图片

注意: 如果返回异常,我们可以排查下面几个点

1、宿主机 ip 是否填写正确

2、redis 有没有设置密码,密码有没有输对

3、检查自己的网是否正常



基本操作示例:

jedis 的操作和 redis-cli 中的操作基本是一致的

熟悉了 redis-cli 的操作,jedis 可以快速上手

  • String:
Jedis jedis = JedisUtil.getInstance();
jedis.set("key3","key3Val");
System.out.println(jedis.get("key3")); //key3Val
  • list:
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]
  • set:
jedis.sadd("set3","redis","redis","mysql");
Set<String> set3 = jedis.smembers("set3");
System.out.println(set3);


2、Jedis 实例,验证码获取

要求:

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));
    }
  }
}

可以发现,请求、验证、次数限制、超时都正常工作:

Redis 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第23张图片



3、springboot 集成 redis

创建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 基础入门 - 安装、基础数据结构、跳表、验证码实现、PV,UV_第24张图片


小结

本篇文章主要介绍了 redis 的一些基础操作,并借助这些基础操作,完成了一个验证码的功能

下一篇,将介绍 redis 分布式锁、秒杀、集群相关的知识,敬请期待

你可能感兴趣的:(中间件,数据库,redis,缓存,中间件,跳表,验证码)