Redis几种数据类型的使用笔记

Redis

关于Redis我们先要问:什么是Redis?

个人认为它是一种key - value 存储系统

数据结构

String 字符串类型: name: "xxx"

List 列表:names: ["cat", "dog"]

Set 集合:names: ["cat", "dog"](值不能重复)

Hash 哈希:person: { "name": "张三", "age": 18 }

Zset 集合:persons: { person1 - 9, peron2 - 12 }(适合做排行榜)

geo(计算地理位置)

hyperloglog(pv / uv)(可以来做页面访问)

pub / sub(发布订阅,类似消息队列)

BitMap (1001010101010101010101010101)(用来做每月签到)

bloomfilter(布隆过滤器,主要从大量的数据中快速过滤值,比如邮件黑名单拦截)

使用

自定义序列化类

​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
​
@Configuration
public class RedisTemplateConfig {
​
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(connectionFactory);
        redisTemplate.setKeySerializer(RedisSerializer.string());
        return redisTemplate;
    }
}

Java中的实现方式

Spring Data Redis(推荐)

1、引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>2.6.4</version>
</dependency>

2、配置Redis地址

spring:
  # redis 配置
  redis:
    port: 6379  //端口号
    host: localhost //主机名
    database: 0 //数据存储在redis的哪个数据库上

Jedis

独立于 Spring 操作 Redis 的 Java 客户端

要配合 Jedis Pool 使用

Lettuce

高阶 的操作 Redis 的 Java 客户端

异步、连接池

Redisson(分布式)

分布式操作 Redis 的 Java 客户端,让你像在使用本地的集合一样操作 Redis(分布式 Redis 数据网格)。就是功能更加丰富版本的 RedisTemplate,经常使用为分布式锁(它解决了分布式系统中的多个节点互斥访问的问题)。

  • 互斥

  • 防止死锁

  • 可重入

  • 自动续期(看门狗机制)

  • 高性能

  • ....

Redisson中的看门狗机制
如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间,这在Redisson中称之为 Watch Dog 机制。
​
如果拿到分布式锁的节点宕机,且这个锁正好处于锁住的状态时,会出现锁死的状态,为了避免这种情况的发生,锁都会设置一个过期时间。这样也存在一个问题,加入一个线程拿到了锁设置了30s超时,在30s后这个线程还没有执行完毕,锁超时释放了,就会导致问题,便有了 watch dog 自动延期机制。
​
Redisson提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期,也就是说,如果一个拿到锁的线程一直没有完成逻辑,那么看门狗会帮助线程不断的延长锁超时时间,锁不会因为超时而被释放。
​
默认情况下,看门狗的续期时间是30s,也可以通过修改Config.lockWatchdogTimeout来另行指定。
​
另外Redisson 还提供了可以指定leaseTime参数的加锁方法来指定加锁的时间。超过这个时间后锁便自动解开了,不会延长锁的有效期
​
阅读源码总结:
1、watch dog 在当前节点存活时每10s给分布式锁的key续期 30s;
2、watch dog 机制启动,且代码中没有释放锁操作时,watch dog 会不断的给锁续期;
3、如果程序释放锁操作时因为异常没有被执行,那么锁无法被释放,所以释放锁操作一定要放到 finally {} 中;

JetCache

比较:

1. 如果你用的是 Spring,并且没有过多的定制化要求,可以用 Spring Data Redis,最方便
2. 如果你用的不是 SPring,并且追求简单,并且没有过高的性能要求,可以用 Jedis + Jedis Pool
3. 如果你的项目不是 Spring,并且追求高性能、高定制化,z可以用 Lettuce,支持异步、连接池
4. 如果你的项目是分布式的,需要用到一些分布式的特性(比如分布式锁、分布式集合),推荐用 redisson

使用场景

1、普通缓存数据

比如:

1)使用string存储 验证码

//这里是项目中存储后端返回给用户的验证码操作
        String code = RandomUtil.randomNumbers(6);
//保存验证码到Redis
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY + phone,code,2, TimeUnit.MINUTES);

2)使用hash存储用户信息(通过这个,加上拦截器中设置,就是redis+session的登录校验)

//1、随机生成token作为登录令牌
        String token = UUID.randomUUID().toString();
//2、将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString())
                );
//3、利用hash存储用户信息,比直接使用 String 结构是更为节省空间
        String tokenKey = RedisConstants.LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
//4、设置token有效期(缓存是非常珍贵的,一般都要设置有效期)
        stringRedisTemplate.expire(tokenKey,RedisConstants.LOGIN_USER_TTL,TimeUnit.MINUTES);

2、消息队列

1)介绍:

消息队列:就是存放消息的队列。它提供了一种高效、可靠、异步的通信方式,能够降低系统的耦合性,提高系统的扩展性、可维护性和可靠性。
个人认为消息队列只是redis的一个补充,而不是重点,这个redis实现消息队列只适合轻量级,而且需要看情况来实现。
​
最简单的消息队列包括3个角色:
1、消息队列:
2、生产者:发送消息到消息队列
3、消费者:从消息队列获取消息并处理
​
生产者 -》 消息队列 -》 消费者
​
生产者:发送消息
消费者:接收消息

2)实现

Redis中提供了3种不同方式实现消息队列:
1、list结构:基于list结构模拟消息队列
    优点:基于Redis持久化,数据安全有保证;消息有序
    缺点:无法避免消息丢失;只能被读一次
2、PubSub:基本的点对点消息模型  (一个生产者可对多个消费者)
    优点:采用发布订阅模型,支持多生产,多消费
    缺点:不支持数据持久化;无法避免数据丢失;消息堆积有限,超限则丢失
3、Stream:比较完善的消息队列模型(5.0以上引入,专门为消息队列而生)
    特点:
        1)可回溯(消息消费完不会被删除)
        2)可以多消费者争抢消息,加速消费
        3)可以阻塞读取
        4)没有消息漏读风险(会标记上次消费的位置)
        5)有消息确认机制,保证消息至少被消费一次
    基于Stream的消息队列——消费者组
# * 表示让 Redis 为插入的数据自动生成一个全局唯一的 ID
# 往名称为 mymq 的消息队列中插入一条消息,消息的键是 name,值是 sid10t
127.0.0.1:6379> XADD mymq * name sid10t
"1665058759764-0"
​
#从消息队列mymq中读取消息
127.0.0.1:6379> XREAD STREAMS mymq 1665058759764-0
(nil)
127.0.0.1:6379> XREAD STREAMS mymq 1665058759763-0
1) 1) "mymq"
   2) 1) 1) "1665058759764-0"
         2) 1) "name"
            2) "sid10t"
            
#通过BLOCK实现阻塞读
# 命令最后的 $ 符号表示读取最新的消息
127.0.0.1:6379> XREAD BLOCK 10000 STREAMS mymq $
(nil)
(10.01s)
​
​
# 创建一个名为 group1 的消费组,0-0 表示从第一条消息开始读取。
127.0.0.1:6379> XGROUP CREATE mymq group1 0-0
OK
# 创建一个名为 group2 的消费组,0-0 表示从第一条消息开始读取。
127.0.0.1:6379> XGROUP CREATE mymq group2 0-0
OK
​
​
# 命令最后的参数“>”,表示从第一条尚未被消费的消息开始读取。
#消息队列中的消息一旦被消费组里的一个消费者读取了,就不能再被该消费组内的其他消费者读取了,即同一个消费组里的消费者不能消费同一条消息。但是不同的消费者组可以再去读这条消息。就是说每个消息只可在消费者组中读一次
127.0.0.1:6379> XREADGROUP GROUP group1 consumer1 STREAMS mymq >
1) 1) "mymq"
   2) 1) 1) "1665058759764-0"
         2) 1) "name"
            2) "sid10t"
​
#使用XPENDING来查看一下 group2 中各个消费者已读取、但尚未确认的消息个数
127.0.0.1:6379> XPENDING mymq group2
1) (integer) 4
2) "1665058759764-0"
3) "1665060634962-0"
4) 1) 1) "consumer1"
      2) "2"
   2) 1) "consumer2"
      2) "1"
   3) 1) "consumer3"
      2) "1"
​
小结:
1、消息保序:XADD/XREAD
2、阻塞读取:XREAD block
3、重复消息处理:Stream 在使用 XADD 命令,会自动生成全局唯一 ID;
4、消息可靠性:内部使用 PENDING List 自动保存消息,使用 XPENDING 命令查看消费组已经读取但是未被确认的消息,消费者使用 XACK 确认消息;
5、支持消费组形式消费数据

3、计数器

1)点赞统计(比如说这下面是一个博客点赞统计)

//1、获取当前用户id
        Long userId = UserHolder.getUser().getId();
//2、判断是否点赞
        String key = RedisConstants.BLOG_LIKED_KEY + id;
        //获得score值,判断当前用户是否对这个博客点赞
        Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());
//3、未点赞,可以点赞
        if(score == null){
 //3.1、数据库的liked点赞数+1
            boolean success = update().setSql("liked = liked +1").eq("id", id).update();
 //3.2、保存用户到redis的set集合,方便判断是否点赞
        if(success){
            stringRedisTemplate.opsForZSet().
            add(key,userId.toString(),System.currentTimeMillis());
            }
        }else{
//4、已经点赞,取消点赞
  //4.1、数据库点赞-1
            boolean success = update().setSql("liked = liked -1").eq("id", id).update();
  //4.2、把用户从redis的set集合删除
            if(success) {
                stringRedisTemplate.opsForZSet().remove(key, userId.toString());
            }
        }

2)网站点击量——UV统计

先搞懂两个概念:

  • UV:独立访问量,只记录某个用户单次

  • PV:页面访问量,记录每次访问网站次数

Hyperloglog是loglog算法派生的概率算法。特点:相比于传统的集合统计方法,HyperLogLog 具有更小的内存占用和更快的计算速度。

 //这里是一个模拟网页点击的测试
    @Test
    void testHyperLogLog(){
        String[] values = new String[1000];//1000个用户
        int j = 0;
        for(int i =0;i<1000000;i++){    //访问页面次数
            j=i%1000;
            values[j] = "user_" + i;
            if(j == 999){
                stringRedisTemplate.opsForHyperLogLog().add("dj",values);
            }
        }
        Long count = stringRedisTemplate.opsForHyperLogLog().size("dj");
        System.out.println(count);  //运行结果:997593,所耗空间:12.02kB
    }

4、分布式锁

借用Redis的SETNX实现分布式锁,确保在分布式系统中的并发安全性

//获取锁
private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
//释放锁
private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }

5、排行榜

这里利用zset实现了一个点赞排行榜,按最先点赞排序

String key = RedisConstants.BLOG_LIKED_KEY + id;
//取出前5个点赞的
        Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
        if(top5 == null || top5.isEmpty()){
            return Result.ok(Collections.emptyList());
        }
//这里就是对取出的数据进行操作了
        List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
        String idStr = StrUtil.join(",", ids);
        List<UserDTO> userDTOs = userService.query().in("id", ids).last("order by field(id," + idStr + ")")
                .list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());

6、共同关注

利用set集合实现

//1、获取当前用户
        Long userId = UserHolder.getUser().getId();
        String key1 = "follows:" + userId;
        String key2 = "follows:" + followUserId;
//2、求交集(前提是双方都把自己关注的对象存储到set集合中了)
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
        if (intersect.isEmpty() || intersect.size() == 0)
            return Result.ok();
//3、取出共同关注的对象的ID
List<Long> ids = intersect
                .stream()
                .map(Long::valueOf)
                .collect(Collectors.toList());

7、地理位置应用GEO

String key = RedisConstants.SHOP_GEO_KEY + typeId;
        //3、查询redis、按照距离排序、分页。  结果:shopId,distance
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs()
                                .includeCoordinates().limit(end)
                );
通过调用 opsForGeo() 方法获取 RedisTemplate 中的 Geo 操作对象,然后使用 search() 方法进行地理位置搜索。其中,GeoReference.fromCoordinate(x, y) 表示搜索以坐标 (x, y) 为中心,半径为 5000 米的范围内的数据;new Distance(5000) 表示搜索的半径为 5000 米;RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end) 表示搜索结果需要包含坐标,并且限制最多返回 end 条记录。最后,调用 search() 方法返回的结果是一个 GeoResults 对象,其中包含了按照距离排序后的店铺 ID 和与中心点的距离(单位为米)。

8、滚动分页

利用zset集合的分数实现滚动分页

 public IPage<User> recommend(Integer pageNum, Integer pageSize, HttpServletRequest request) {
        User loginUser = getLoginUser(request);
        String key = String.format("user:recommend:%s", loginUser.getId());
​
        long minScore = (pageNum - 1) * pageSize;
        long maxScore = pageNum * pageSize - 1;
​
        // 从缓存中获取指定分数范围内的数据,按分数从高到低排序
        Set<ZSetOperations.TypedTuple<Object>> typedTuples = redisTemplate.opsForZSet()
                .reverseRangeByScoreWithScores(key, minScore, maxScore);
​
        if (!typedTuples.isEmpty()) { // 缓存命中,直接返回缓存数据
            return parseResult(typedTuples);    //parseResult方法是一个将typedTuples解析成想要的数据
        }
​
        // 无缓存,查询数据库
        Page<User> page = new Page<>(pageNum, pageSize);
        QueryWrapper<User> queryWrapper = new QueryWrapper<>();
        IPage<User> userPage = page(page, queryWrapper);
​
        // 将查询结果存入缓存,使用与查询时相同的分数范围
        int count = 0;
        for (User user : userPage.getRecords()) {
            count++;
            if (count >= 10 && minScore == 0){
                minScore += 10;
            }
            redisTemplate.opsForZSet().add(key, user, minScore); // 使用最小分数存入缓存,每10个分成了一组的数据
        }
        redisTemplate.expire(key, 30, TimeUnit.MINUTES);
​
        return userPage;
    }

9、每月签到

BitMap实现获取本月签到次数

// 用户签到
    public void signIn(String userId, String date) {
        String key = "signin:" + date; // 每天一个 key,用于记录当天的签到情况
        redisTemplate.opsForValue().setBit(key, Long.parseLong(userId), true);
    }
​
// 判断用户是否已签到
    public boolean hasSignedIn(String userId, String date) {
        String key = "signin:" + date;
        return redisTemplate.opsForValue().getBit(key, Long.parseLong(userId));
    }
    
// 获取本月总签到次数
    public long getTotalSignInCountThisMonth() {
        LocalDate now = LocalDate.now();
        int daysInMonth = now.lengthOfMonth();
        long totalSignInCount = 0;
​
        for (int day = 1; day <= daysInMonth; day++) {
            String key = "signin:" + now.format(DateTimeFormatter.ofPattern("yyyyMM")) + String.format("%02d", day);
            totalSignInCount += redisTemplate.opsForValue().bitCount(key);
        }
​
        return totalSignInCount;
    }

10、用户唯一标识

一共有3种方法实现:

  • 数据库自增

  • UUID

  • redis实现

通过位运算符将时间戳和序列号合并成一个 64 位的二进制数,其中时间戳占据高 32 位,序列号占据低 32 位。

public class RedisIdWorker {
    private StringRedisTemplate stringRedisTemplate;
​
    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
​
    private static final long BEGIN_TIMESTAMP = 1702762344L;
    private static final int COUNT_BITS =32;
​
    public long nextID(String keyPrefix){
        //1.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond - BEGIN_TIMESTAMP;
        //2.生成序列号
        //2.1获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        //2.2自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);
        //3.拼接并返回
        return timeStamp << COUNT_BITS | count;
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值