Redis学习笔记

Redis学习笔记

一 .Redis简单介绍:

1.Redis是什么?

Redis是一个用C语言编写,支持网络,持久化,数据以键值对Key-Value的形式存储的开源数据库。

为了满足不同的业务场景,Redis 内置了多种数据类型实现比如

5 种基础数据结构 :String(字符串)、List(列表)、Set(集合)、Hash(散列)、Zset(有序集合)。

3 种特殊数据结构 :HyperLogLogs(基数统计)、Bitmap (位存储)、Geospatial (地理位置)。

Redis 速度快较重要的主要有下面 3 点:

  • Redis 基于内存,内存的访问速度是磁盘的上千倍;
  • Redis 基于 Reactor 模式设计开发了一套高效的事件处理模型,主要是单线程事件循环和 IO 多路复用
  • Redis 内置了多种优化过后的数据结构实现,性能非常高。
2.Redis可以做什么?

(1)缓存,毫无疑问这是Redis当今最为人熟知的使用场景。再提升服务器性能方面非常有效;

(2)排行榜,如果使用传统的关系型数据库来做这个事儿,非常的麻烦,而利用Redis的SortSet数据结构能够非常方便搞定;

(3)计算器/限速器,利用Redis中原子性的自增操作,我们可以统计类似用户点赞数、用户访问数等,这类操作如果用MySQL,频繁的读写会带来相当大的压力;限速器比较典型的使用场景是限制某个用户访问某个API的频率,常用的有抢购时,防止用户疯狂点击带来不必要的压力;

(4)好友关系,利用集合的一些命令,比如求交集、并集、差集等。可以方便搞定一些共同好友、共同爱好之类的功能;

(5 )简单消息队列,除了Redis自身的发布/订阅模式,我们也可以利用List来实现一个队列机制,比如:到货通知、邮件发送之类的需求,不需要高可靠,但是会带来非常大的DB压力,完全可以用List来完成异步解耦;

(6)Session共享,默认Session是保存在服务器的文件中,即当前服务器,如果是集群服务,同一个用户过来可能落在不同机器上,这就会导致用户频繁登陆;采用Redis保存Session后,无论用户落在那台机器上都能够获取到对应的Session信息。

(7)使用Redis中Bitmap数据结构可以做用户签到功能,32位对应一个月30或31天(1表示签到成功,0失败)

(8) 使用Redis中Geospatial 数据结构可以做用户地理位置相关操作例如附近商店,附近的人等

(9) 使用Redis中HyperLogLogs数据结构可以做统计相关才做,比如今日网页用户活跃用户量等

二.在Java开发中使用Redis

1.在pom文件中导入相关坐标
<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.7.RELEASE</version>
</dependency>
   <!--redisson-->
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
</dependency>
2.配置连接的application.yml文件
  redis:
    host: xxx.xxx.xxx.xxx
    port: 639
3.Redisson配置类
@Configurationpublic 
class RedissonConfig {   
    @Bean   
    public RedissonClient redissonClient(){   
        // 配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://xxx.xxx.xxx.xxx:6379"); //开启redis的主机ip:端口号
        // 创建RedissonClient对象
        return Redisson.create(config);
    }
4.测试
@SpringBootTest
public class testRedis {

    @Resource
    private RedisTemplate redisTemplate;  //k- v 都是对象
    
     @Resource
    private StringRedisTemplate stringRedisTemplate; //操作 k-v 字符串

    @Test
    void test(){
        redisTemplate.opsForValue();//操作字符串
        redisTemplate.opsForHash();//操作hash
        redisTemplate.opsForList();//操作list
        redisTemplate.opsForSet();//操作set
        redisTemplate.opsForZSet();//操作有序set
    }
}

三.Redis个别业务场景代码实现

1.基于Redis生成Token实现用户登录功能

登录接口实现代码(之前需要使用验证码发送接口将用户验证码缓存到Redis中)

@Override
public Result login(LoginDTO loginDto, HttpSession session) {
    // 1.校验手机号
    String phone = loginDto.getPhone();
        //正则表达式方法校验手机号码格式
    if (RegexUtils.isPhoneInvalid(phone)) {
        // 2.如果不符合,返回错误信息
        return Result.fail("手机号格式错误!");
    }
    // 3.从redis获取验证码并校验  此前在发送验证码的时候将("login_code_key:" + phone)作为key放入redis中
    String cacheCode = stringRedisTemplate.opsForValue().get("login_code_key:" + phone);
    String code = loginDto.getCode();
    if (cacheCode == null || !cacheCode.equals(code)) {
        // 不一致,报错
        return Result.fail("验证码错误");
    }
    // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?
    User user = query().eq("phone", phone).one();
    // 5.判断用户是否存在
    if (user == null) {
        // 6.不存在,创建新用户并保存
        user = createUserWithPhone(phone);
    }
    // 7.保存用户信息到 redis中
    // 7.1.随机生成token,作为登录令牌
    String token = UUID.randomUUID().toString(true);
    // 7.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()));
    // 7.3.存储
    String tokenKey = "login_code_key:"  + token;
    stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
    // 7.4.设置token有效期               //自定义常量过期时间,时间单位
    stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);
    // 8.返回token
    return Result.ok(token);
}

配置拦截器在用户每次访问其他页面时候刷新Token


public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)) {
            //如果为空说明用户还没有登录,返回true进入登录页面
            return true;
        }
        // 2.基于TOKEN获取redis中的用户
        String key  =  "login_code_key:" + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 4.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 5.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 6.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 7.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}

配置拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1); // 后面的order是拦截顺序,数字越小优先级越高
        // token刷新的拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
            .addPathPatterns("/**").order(0);
    }
}
2.Redis缓存存在的常见问题

缓存雪崩:

大量缓存数据同时间失效,导致用户直接发起大量请求到数据库,产生瓶颈。
1、生成随机失效的缓存时间数据;
2、让缓存节点分布在不同的物理节点上;
3、生成不失效的缓存数据;
4、定时任务更新缓存数据;

解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存穿透:

用户请求数据,例如ID为负数,不存在缓存里,也不存在数据库里,会造成缓存穿透。
1、无意义数据放入缓存,下一次相同请求就会命中缓存;
2、IP过滤;
3、参数校验;
4、布隆过滤器;

解决方案:

核心思路如下:

在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的。

现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。

实现代码:

    // 解决缓存穿透
    //大量用户请求数据,例如ID为负数,不存在缓存里,也不存在数据库里,造成数据库压力过大导致缓存穿透。
    //调用接口
    public Result queryById(Long id) {
        Shop shop = cacheClient.queryWithPassThrough(
                                CACHE_SHOP_KEY,
                                id,
                                Shop.class,
                                this::getById,
                                CACHE_SHOP_TTL,
                                TimeUnit.MINUTES);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }

    //封装后的方法
    public <R,ID> R queryWithPassThrough(
            String keyPrefix,                //缓冲Redis中的key前缀可以不传 
            ID id,                           //id查询对象id
            Class<R> type,                   //对象被封装的实体类
            Function<ID, R> dbFallback,      //流调函数 用来做数据库操作
            Long time,                       //缓存过期时间
            TimeUnit unit                    //时间单位
    ){
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type); //json转成实体类
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

缓存击穿:

由于缓存热点键到时失效导致用户请求直接访问数据库
1、用久缓存;
2、分布式锁
a.单体应用—>互斥锁—>zookeeper ,redis实现。

解决方案一、使用锁来解决:

因为锁能实现互斥性。假设线程过来,只能一个人一个人的来访问数据库,从而避免对于数据库访问压力过大,但这也会影响查询的性能,因为此时会让查询的性能从并行变成了串行,我们可以采用tryLock方法 + double check来解决这样的问题。

假设现在线程1过来访问,他查询缓存没有命中,但是此时他获得到了锁的资源,那么线程1就会一个人去执行逻辑,假设现在线程2过来,线程2在执行过程中,并没有获得到锁,那么线程2就可以进行到休眠,直到线程1把锁释放后,线程2获得到锁,然后再来执行逻辑,此时就能够从缓存中拿到数据了。

代码实现:

 // 互斥锁解决缓存击穿
 //调用接口
    public Result queryWithMutex(Long id) {
        Shop shop = cacheClient.queryWithPassThrough(
                                CACHE_SHOP_KEY,
                                id,
                                Shop.class,
                                this::getById,
                                CACHE_SHOP_TTL,
                                TimeUnit.MINUTES);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }
    //实现方法
    public <R, ID> R queryWithMutex(
            String keyPrefix, 
            ID id,
            Class<R> type, 
            Function<ID, R> dbFallback, 
            Long time, 
            TimeUnit unit
    ) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }
        // 4.实现缓存重建
        // 4.1.获取互斥锁   
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                //重新实行该方法直到成功
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis  set方法
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }
   //上锁方法
   private boolean tryLock(String key) {             //setIfAbsent实现上锁功能,如果key存在则上锁失败
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }
   //解锁方法
    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
   //set方法
   public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }  


解决方案二、使用逻辑过期来解决:

方案分析:我们之所以会出现这个缓存击穿问题,主要原因是在于我们对key设置了过期时间,假设我们不设置过期时间,其实就不会有缓存击穿的问题,但是不设置过期时间,这样数据不就一直占用我们内存了吗,我们可以采用逻辑过期方案。

我们把过期时间设置在 redis的value中,注意:这个过期时间并不会直接作用于redis,而是我们后续通过逻辑去处理。假设线程1去查询缓存,然后从value中判断出来当前的数据已经过期了,此时线程1去获得互斥锁,那么其他线程会进行阻塞,获得了锁的线程他会开启一个 线程去进行 以前的重构数据的逻辑,直到新开的线程完成这个逻辑后,才释放锁, 而线程1直接进行返回,假设现在线程3过来访问,由于线程线程2持有着锁,所以线程3无法获得锁,线程3也直接返回数据,只有等到新开的线程2把重建数据构建完后,其他线程才能走返回正确的数据。

这种方案巧妙在于,异步的构建缓存,缺点在于在构建完缓存之前,返回的都是脏数据。

 //调用接口
    public Result queryWithLogicalExpire(Long id) {
        Shop shop = cacheClient.queryWithPassThrough(
                                CACHE_SHOP_KEY,
                                id,
                                Shop.class,
                                this::getById,
                                CACHE_SHOP_TTL,
                                TimeUnit.MINUTES);
        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }
    //实现方法
    // 逻辑过期解决缓存击穿
    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix,
            ID id,
            Class<R> type,
            Function<ID, R> dbFallback,
            Long time, 
            TimeUnit unit
    ) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (!StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json,type);
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }
    //上锁方法同上
    //逻辑过期set方法
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(redisData));
    }

四.linux系统操作Redis相关笔记

Redis启动方式
 cd /usr/local/redis/bin                                        #   启动目录
./redis-server /usr/local/redis/bin/redis.conf                  #   启动命令 

Redis客户端启动方式:
 cd /usr/local/redis/bin           #启动目录
./redis-cli                        #启动命令

Redis关闭

服务端:

1.查看redis进程

 ps -ef | grep redis 

2.杀死redis进程

kill -9 Redis_PID(redis的pid)

客户端

 redis-cli shutdown 

Redis开放端口号
(1)查看对外开放的端口状态

查询已开放的端口

netstat -anp

查询指定端口是否已开 提示 yes,表示开启;no表示未开启。

 firewall-cmd --query-port=6379/tcp 

(2)查看防火墙状态
#查看防火墙状态 
systemctl status firewalld
#开启防火墙 
systemctl start firewalld
#关闭防火墙 
systemctl stop firewalld
#开启防火墙
service firewalld start
#若遇到无法开启先用:
systemctl unmask firewalld.service
#然后:
systemctl start firewalld.service

(3)对外开发端口
#查询对外开放的指定端口
firewall-cmd --query-port=6379/tcp
#添加指定需要开放的端口: 
firewall-cmd --add-port=6379/tcp --permanent
#重载入添加的端口: 
firewall-cmd --reload
#查询指定端口是否开启成功: 
firewall-cmd --query-port=6379/tcp
#除指定端口: 
firewall-cmd --permanent --remove-port=6379/tcp

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值