Redis入门详解学习笔记

Redis命令

切记。打开五个服务

shutdown -r now :重启虚拟机

1.首先安装Redis的依赖:
yum install -y gcc tcl

2.进入到 cd /usr/local/src 目录下,
下载redis镜像文件 解压包在 /usr/local/src/redis-6.2.6中

解压镜像文件 tar -zxvf redis-6.2.6
进入 redis cd redis-6.2.6

3.编译并且安装 make && make install

使用make安装命令成功后安装默认的安装路径是在 /usr/local/bin

在 /usr/local/src/redis-6.2.6中有 redis.conf文件,可以先将此文件拷贝一份
cp redis.conf redis.conf.bck

解决防火墙未安装:

安装防火墙:yum install firewalld firewall-config

image-20220517145638441

img

set key value   ###可以对value进行覆盖
setnx key value ###只有在key不存在的时候才能设置成功, 不可以对value进行覆盖
incry k2 2  ###   数值型k2 +2
decry k2 2   #减去2

mset k1 v1 k2 v2
mget k1 k2 
msetnx k1 v1 k2 v2   ##原子性操作任何一个失败都失败

getrange name 0 3 :   ###获取name中下标从0到3的字符串
setrange name 3 abc:   ###从下标为3开始替换成abc

setex <key> <时间秒> <value>  ##设置key value的生存周期
ttl <key>          ##得出生存周期的时间  -2过期,-1永久

getset  age <value>  ##返回新值,实际上已经使用value替换了旧value
Set集合
相当于value为空
sadd <key> <value1> <value2>...

image-20220513172924471

Hash

Linux下基本文件命令

: w 保存

命令模式下:

wq!保存退出 强制

q! 强制退出

安装后重点文件说明:

/usr/local/redis-4.0.0/src/redis-server:Redis服务启动脚本

/usr/local/redis-4.0.0/src/redis-cli:Redis客户端脚本

/usr/local/redis-4.0.0/redis.conf:Redis配置文件

如果需要远程连接linux,则需要将bind:127.0.0.1注释掉,并且将保护模式关闭:pretoected mode no

进入redis.conf配置文件 在命令模式下输入斜杠 / 输入bind进行搜索 按n键可以搜索下一个。

#开启redis服务器并且制定配置文件 src/redis-server ./redis.conf

#开启服务器并且输入密码 src/redis-cli -h localhost -p 6379

不指定密码:

[root@localhost redis-4.0.0]# ./src/redis-cli
127.0.0.1:6379> keys *
(error) NOAUTH Authentication required.
127.0.0.1:6379> auth 123456
OK
127.0.0.1:6379>

[root@localhost redis-4.0.0]# vim redis.conf
[root@localhost redis-4.0.0]# ps -ef |grep redis
root      86172      1  0 17:32 ?        00:00:01 src/redis-server 127.0.0.1:6379
root     110471 109294  0 17:50 pts/0    00:00:00 grep --color=auto redis
[root@localhost redis-4.0.0]# kill -9 86172   
[root@localhost redis-4.0.0]# src/redis-server ./redis.conf   
110999:C 02 May 17:51:21.517 # oO0OoO0OoO0Oo Redis is starting oO0OoO0OoO0Oo
110999:C 02 May 17:51:21.517 # Redis version=4.0.0, bits=64, commit=00000000, modified=0, pid=110999, just started
110999:C 02 May 17:51:21.517 # Configuration loaded
[root@localhost redis-4.0.0]# src/redis-cli -h localhost -p 6379 
localhost:6379> keys *
(error) NOAUTH Authentication required.
localhost:6379> auth 123456
OK

事务和锁的机制

  1. Redis 事务的主要作用急速串联多个命令,防止别的命令插队。
  2. 原理,使用multi进行组队:
  • 将命令放入队列中等待执行。
  • 当执行Exec的时候才会执行
  • 如果执行discard那么整个队列的命令都不执行。
  • 组队中如果谁有错误,那么所有的命令都不执行,如果执行中谁有错误谁就不执行。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KQOiNvrH-1652805727556)(E:/TYpora%E7%AC%94%E8%AE%B0/image/image-20220514140626007.png)]

multi 进行批处理, 使用队列形式进行处理,组队中有失败的命令也有成功的命令。

exec:将组队的命令全部执行。

discard :将multi批处理的数据全部回滚删除。

使用multi进行命令组队的时候如果任何一个命令出错了,那么其余命令都会回滚,不会执行。

127.0.0.1:6379> multi
OK
127.0.0.1:6379> set b1 v1
QUEUED
127.0.0.1:6379> set b2 v2
QUEUED
127.0.0.1:6379> set b3
(error) ERR wrong number of arguments for ‘set’ command
127.0.0.1:6379> exec
(error) EXECABORT Transaction discarded because of previous errors.
127.0.0.1:6379> keys *
(empty list or set)

image-20220516180546499

Redis超卖和超时问题
  1. 连接超时问题可以使用连接池进行解决
  2. 超卖(<0) 可以使用事务(乐观锁)进行解决 multi
String kcKey = "sk:"+prodid + ":qt";   //库存id
String userKey = "sk:" +prodid + ":user";   //秒杀成功用户

//监控库存
jedis.watch(kcKey);  //监控库存可以使得多个multi只有一个exec能够根据版本执行 乐观锁

//获取库存
String kc = jedis.get(kcKey);
if(kc == null){
  System.out.printfln("秒杀还没有开始!");
  jedis.close();
  return false;
}

//秒杀事务
//使用事务控制
 Transaction multi = jedis.multi();
 //组队操作
multi.decy(kcKey); //将库存减一
multi.sadd(userKey,uid); //将秒杀用户存入Set中

//执行
List <Object> results = multi.exec();
if(results == null ||results.size() ==0){
  System.out.println("秒杀失败了!");
  jedis.close();
  return false;
}
Redis乐观锁出现库存遗留问题
场景:

当一千个人秒杀商品的时候,其中一个人秒杀成功,将乐观锁版本号进行更改,那么其余的人便不能继续做操作,使得出现库存遗留问题。

解决方案

使用LUA脚本。

持久化-RDB(RedisDataBase)

原理

通过一个Fork的子进程建立一个临时RDB文件,来替换持久化的文件 (写时复制技术)

可以设置多少秒内有多少个key进行改变就能替换持久化文件:比如20s内至少替换三个。

缺点

最后一次持久化,服务器挂掉,RDB还没来得及写入持久化文件可能会造成数据丢失。

持久化-AOF

主从复制

一主多从:
  1. 首先将redis.conf文件复制一份到根目录
[root@localhost myredis]# cp /usr/local/redis-4.0.0/redis.conf /myredis/redis.conf
[root@localhost myredis]# ll
总用量 60
-rw-r--r--. 1 root root 57762 514 16:59 redis.conf

  1. 复制三份不同端口号的redis.conf文件
vi redis6379.conf

include /myredis/redis.conf
pidfile /var/run/redis_6381.pid
port 6381
dbfilename dump6381.rdb
replicaof 127.0.0.1 6379 
masterauth 123456    #主机的密码

replicaof 表明自己是从机,他的主机的ip地址是127.0.0.1(本机),端口号是6379。
masterauth是主机的密码。



vi redis6380.conf
vi redis6381.conf   都修改成这个内容
  1. 启动三个redis服务器
[root@localhost myredis]# redis-server redis6379.conf

[root@localhost myredis]# redis-server redis6380.conf

[root@localhost myredis]# redis-server redis6381.conf

[root@localhost myredis]# ps -ef |grep redis
root      37672      1  0 17:17 ?        00:00:00 redis-server *:6379
root      37972      1  0 17:18 ?        00:00:00 redis-server *:6380
root      38187      1  0 17:18 ?        00:00:00 redis-server *:6381
root      38445 111035  0 17:18 pts/2    00:00:00 grep --color=auto redis

  1. 开启三个Redis客户端连接
 redis-cli -p 6381
 redis-cli -p 6380
 redis-cli -p 6379

  1. 输入info replication查看当前是主还是从
127.0.0.1:6381> info replication
# Replication
role:master    #master主
connected_slaves:0
master_replid:84ae290048a52f161387e91e9c6c0f026a72df66
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

  1. 配置从机
slaveof ip地址 端口号      #设置为指定服务器的从机
slaveof no one ##将从机变为主机

127.0.0.1:6380> slaveof 127.0.0.1 6379    #设定为 127.0.0.1 端口号为6379的从机
OK
127.0.0.1:6380> info replication     #查看信息
# Replication
role:slave      #当前为从机
master_host:127.0.0.1    #主机ip
master_port:6379          #主机端口
master_link_status:down    
master_last_io_seconds_ago:-1
master_sync_in_progress:0
slave_repl_offset:1
master_link_down_since_seconds:1652521252
slave_priority:100
slave_read_only:1
connected_slaves:0
master_replid:c799cd475739fc5c61e790c0442cd1be8efb91f5
master_replid2:0000000000000000000000000000000000000000
master_repl_offset:0
second_repl_offset:-1
repl_backlog_active:0
repl_backlog_size:1048576
repl_backlog_first_byte_offset:0
repl_backlog_histlen:0

  1. 不能在从机上做写操作,只能做读操作
127.0.0.1:6381> keys *
(empty list or set)
127.0.0.1:6381> set a2 v2
(error) READONLY You can't write against a read only slave.
127.0.0.1:6381> 

主从复制的原理

image-20220514204635338

哨兵机制

当主机宕机后,往从机中选择一个作为主机。
  1. 在myredis目录下 新建一个 sentinel.conf文件
  2. 在文件中配置 sentinel monitor mymaster 127.0.0.1 3682 1 :其中 1 为同意迁移的数量 其中mymaster为取的名称
  3. 开启redis的哨兵监控

redis-sentinel /myredis/sentinel.conf

  1. 当为redis设置了密码,同样哨兵检测配置文件也需要设置,sentinel auth-pass <password
sentinel monitor mymaster 127.0.0.1 3682 1       ###设置哨兵监控主服务器
 
sentinel auth-pass  mymaster 123456      #在sentinel.conf配置文件中设置服务器的密码
  1. 执行命令 ‘redis-sentinel /myredis/sentinel.conf’

可以看到,当 6379 主服务器宕机以后,哨兵会通过投票,将 6381 服务器选举为新的“master”服务器,并且另外一个 6380 的从服务器也自动归属在 6381 服务器下。

主服务器宕机后,哨兵选取从服务器作为主服务器的规则

偏移量指的是和主服务器同步量多的

搭建Redis集群

  1. 在六个redis.conf文件中添加如下配置:
cluster-enabled yes     #开启集群
cluster-config-file nodes-6379.conf    #设置结点的配置文件名称
cluster-node-timeout 15000          #设置结点失联时间 超过该时间 毫秒 集群自动进行主从切换

  1. 开启六个地址的服务器

image-20220515113802305

  1. 进入到本地用户的bin目录下启动 (遇到的问题)

    **注意:**redis版本 4.0 ;
    问题:Unrecognized option or bad number of args for: ‘–cluster’
    解决方案:不能在安装目录下启动,需要在bin目录下启动

    cd /usr/local/bin
    /redis的安装目录/src/redis-cli --cluster create --cluster-replicas 1 ip:端口 ip:端口号
    

    image-20220515115619789.png

    1. 集群中主机挂掉,从机变为主机,如果主机再启动,那么启动的这个主机就变为从机。如果该结点的主机从机全部宕机了,那么根据配置看该集群是否还可用。

    image-20220515122517077

    1. 集群的好处和不足

    image-20220515123141908

    (173条消息) Redis及其Sentinel配置项详细说明_a1282379904的博客-CSDN博客_failover-timeout

    (173条消息) 八、Sentinel.conf 配置文件详细介绍_胖太乙的博客-CSDN博客_sentinel 配置文件

缓存穿透

概念

当访问量过大,服务器压力变大,去查询Redis的时候命中率变低 (即查不到数据) 就会去查询数据库

出现很多不正常的url 即搜索不存在的数据,造成服务器瘫痪。

image-20220515123619414

解决方案

image-20220515124110299

image-20220515124119395

缓存击穿

image-20220515124703516

解决方案

image-20220515124817561

缓存雪崩

概念

在极少时间段内,查询大量key集中过期的情况。

分布式锁

1. setnx users 10   #nx表示上锁
   del users        #释放锁
 
2.  #为了保证上了锁一直没有释放锁 给所设置个过期时间
3. 为了保证原子操作,将上锁和设置过期时间一起执行
 set users 10 nx ex 12   #ex表示设置过期时间为12s

image-20220515141810634

实操

Redis配置文件
package com.example.springboot_redis.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {

    @Bean
     public RedisTemplate<Object,Object> redisTemplate(RedisConnectionFactory connectionFactory){
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
//        设置连接工厂
        redisTemplate.setConnectionFactory(connectionFactory);

//         设置JSON的序列化工具  不必手动使用ObjectMapper进行映射
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();

//       设置Key的序列化
        redisTemplate.setKeySerializer(new StringRedisSerializer());
          redisTemplate.setHashKeySerializer(new StringRedisSerializer());

//        设置Value额序列化
        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);


          return redisTemplate;
    }

}

hutool工具类

StrUtil.isNotBlank(shopJson)
BooleanUtil.isTrue(flag);     //使用hutool工具类来判断返回能够拆封装防止空指针
  
Shop shop = JSONUtil.toBean(shopJson,Shop.class);
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));//将实体类转化为JSON

1.将用户信息以Map格式存储到Redis//        7.1 将User对象转成Hash存储
        UserDTO userDTO  = BeanUtil.copyProperties(user,UserDTO.class); //将user的信息复制到UserDTO中
//        7.2将userDTO转化为Map结构方便一次性存入Redis中
Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
           CopyOptions.create().setIgnoreNullValue(true) //允许有空值
                         .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())//将value转为String
        );
//        7.3存储Redis中
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,map);  //将map结构存入Redis中



2.将查询到的用户信息以Map格式填充为UserDTO类
  ap<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
//      判断用户是否存在
        if (userMap.isEmpty()){
            return  true;
        }
//        5.将查询到的Hash数据转换为usrDTO对象   将userMap填充为UserDTO 这种类型,并且报异常
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);
	
Session在Tomcat中不共享的问题

image-20220515203706343

保存用户数据方案:

image-20220515204120183

Token令牌

image-20220515204410232

前端token用例
  • 当登录请求响应回来会带上登录凭证Token,将登录凭证Token存储到浏览器的sessionStorage中

image-20220515204555460

image-20220515204821607

注册、登录 带Token
UserServiceImpl@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

  //发送验证码
    @Override
    public Result sendCode(String phone, HttpSession session) {
//  1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
//          如果不符合,返回错误信息
            return Result.fail("手机格式错误!");
        }
//         3.符合,生成验证码
//       3.1调用hutool工具类
        String code = RandomUtil.randomNumbers(6);

//         4.保存验证码到Redis 并且设置过期时间为2分钟
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);
//        5.发送验证码
      log.debug("发送短信验证码成功,验证码:{}"+code);
      
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
      String phone = loginForm.getPhone();
      if (RegexUtils.isPhoneInvalid(phone)){
//        校验手机号码格式,不符合格式直接返回
          return Result.fail("手机格式有误!");
      }
//     校验验证码  从Redis中获得验证码
       String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY+phone);
       String code = loginForm.getCode();
       if (cacheCode ==  null || !cacheCode.toString().equals(code)){
//         验证码不一致 报错
           return Result.fail("验证码错误");
       }

//     根据手机号查询用户
      User user =  query().eq("phone",phone).one();  //根据手机号查询一个用户

        if (user == null){
           user =  createUserWithPhone(phone);
        }

//     7      保存用户信息到Redis
        String token = UUID.randomUUID().toString(true);  //使用hutool中的UUID随机生成一个不带-的token

//        7.1 将User对象转成Hash存储
        UserDTO userDTO  = BeanUtil.copyProperties(user,UserDTO.class);
//        7.2将userDTO转化为Map结构方便一次性存入Redis中
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                 CopyOptions.create().setIgnoreNullValue(true) //允许有空值
                         .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString())//将value转为String
        );
//        7.3存储Redis中
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,map);  //将map结构存入Redis中

//        7.4设置token的有效期
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);

//        7.5  登录续期  在拦截器中做登录续期, 如果能够通过拦截器表示该用户登录,进行续期

        session.setAttribute("user", BeanUtil.copyProperties(user,UserDTO.class));  //将user拷贝到UserDto中

        return Result.ok();
    }
    private User createUserWithPhone(String phone){
//       创建用户
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX+RandomUtil.randomString(10));

        save(user);
        return user;

    }
}

  • MvcConfig 用来注册拦截器
package com.hmdp.config;

import com.hmdp.utils.LoginInterceptor;
import com.hmdp.utils.ReflushTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * 注册拦截器    添加配置的拦截器
 */
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    //这里可以自动注入,因为Configuration交给容器
    @Autowired
    StringRedisTemplate redisTemplate;

    //第二个拦截器,用来做请求是否放行  order是拦截器的级别默认为0,越低越高
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(        //添加放行请求路径
                   "/shop/**",
                    "voucher/**",
                        "/shop-type/**",
                        "upload/**",
                        "blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);

        //第一个拦截器,用来做Redis续期
        registry.addInterceptor(new ReflushTokenInterceptor(redisTemplate))
                .addPathPatterns("/**").order(0);
    }
}

  • 线程包装类
package com.hmdp.utils;
import com.hmdp.dto.UserDTO;
/**
 * 线程包装类
 */

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}
拦截器就采用下面那种方案,使用两个拦截器。

拦截器采用责任链模式,当某个拦截器返回false,那么所有的post都不会被执行,直接返回前一个拦截器的ater方法,因为post是目标请求执行之前处理,而目标请求被拦截了,而且又是责任链模式(即先执行所有拦截器的pre方法,再执行post) 那么post也不会被执行

问题:当用户一直访问详情页、主页等不需要登录就能看的页面无法刷新生命周期,那么30分钟之后就会过期。
解决方案:

设置两个拦截器,第一个用来刷新Redis声明周期,第二个拦截器用来控制拦截状态

  • 第一个拦截器用来刷新用户的登录时间
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * 统一放行,到第二个拦截器再判断是否拦截
 */
public class ReflushTokenInterceptor implements HandlerInterceptor {

    //这里不能@Autowired 进行注入,因为拦截器配置器那里的对象是new出来的,只能使用构造器
     private StringRedisTemplate stringRedisTemplate;

    public ReflushTokenInterceptor(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)) {
            return true;
        }
//       基于TOKEN获取Redis中的用户
        String key = RedisConstants.LOGIN_USER_KEY+token;

        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY + token);
//      判断用户是否存在
        if (userMap.isEmpty()){
            return  true;
        }
//        5.将查询到的Hash数据转换为usrDTO对象   将userMap填充为UserDTO 这种类型,并且报异常
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap,new UserDTO(),false);

//        存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);

//        7.对Redis用户信息进行续期
        stringRedisTemplate.expire(key,RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;


    }
    
    /**
     * 第一个拦截器为结尾,移除当前用户   视图渲染结束之后执行
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */

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

    }
}

  • 第二个拦截器用来校验是否放行
package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


public class LoginInterceptor implements HandlerInterceptor {


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//    判断ThreadLocal中是否有用户 来进行拦截

        if (UserHolder.getUser() == null){
//            没有用户就进行拦截

            response.setStatus(401);
            return false;
        }

//      有用户,放行
        return true;

    }
}

Redis缓存商户应用场景
使用场景:

根据请求的商户Id去查询Redis,判断缓存是否命中,命中的话就直接返回给客户端,如果未命中就查询mysql数据库,并且将查询的数据缓存进Redis。

对Redis做修改操作应用场景
缓存和数据库的双写一致

需要先对Redis做更新,再去操作数据库

 public Result queryById(Long id) {
        String key = CACHE_SHOP_KEY + id;
//         1.从Redis中查询商铺的缓存
         String shopJson = stringRedisTemplate.opsForValue().get(key);
//         2.判断是否存在该商铺
        if (StrUtil.isNotBlank(shopJson)){
//           存在就转成Bean并且返回
            Shop shop = JSONUtil.toBean(shopJson,Shop.class);
            return Result.ok(shop);
        }
//        3.不存在就从sql中查询
        Shop shop = getById(id);
        if (shop == null){
            return Result.fail("该店铺不存在!");
        }
//        存在,写入Redis, 但要将实体类转化为JSON字符串
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

    /**
     * 更新店铺, 需要先查询更新缓存,再操作数据库
     * @param shop
     * @return
     */
    @Override
    @Transactional
    public Result update(Shop shop) {

        Long id =shop.getId();
        if(id == null){
            return Result.fail("该店铺不存在");
        }
        //     1. 更新数据库
        this.updateById(shop);

//       2. 删除缓存
       stringRedisTemplate.delete(CACHE_SHOP_KEY+id);
       return Result.ok();
    }

解决缓存穿透的使用场景

  1. 缓存null值

  2. 布隆过滤

思路:

在sql中查询,如果查询不到该商铺,就将空字符串""写入redis中

在请求redis的时候:

  • 如果查询的shopJson是一个空字符串,那么表示该商铺不存在
  • 如果查询到null,表示redis中没有该商铺,那么就继续向mysql中进行查询
  • 如果查询到数据了,那么就直接返回数据。

查询redis,如果命中了数据直接返回数据,如果命中空字符串返回null,如果数据和空字符串都没命中,那么就查数据库

实现代码:

    //封装缓存穿透
   public Shop queryPassThrough(Long id){
       String key = CACHE_SHOP_KEY + id;
//         1.从Redis中查询商铺的缓存
       String shopJson = stringRedisTemplate.opsForValue().get(key);
//         2.判断是否存在该商铺
       if (StrUtil.isNotBlank(shopJson)){  //只有字符串才是true
//           存在就转成Bean并且返回
           return JSONUtil.toBean(shopJson,Shop.class);
       }
//     判断命中的是否是空值
       if(shopJson != null){
//        如果当前不是null,是空字符串,就是之前查数据库查不到的店铺,就返回错误信息
           return null;
       }

//        3.不存在就从sql中查询
       Shop shop = getById(id);
       if (shop == null){
//          从数据库中查询不到该商铺就写入Redis一个空字符串
           stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
           return null;
       }
//        存在,写入Redis, 但要将实体类转化为JSON字符串
       stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
       return shop;
   }

缓存击穿问题
使用互斥锁来解决缓存击穿
互斥锁,使用setnx lock 1   只有不存在的时候才能设置成功,当释放锁后才能继续被赋值
127.0.0.1:6379> setnx lock 2
(integer) 1
127.0.0.1:6379> setnx lock 2     ####锁没被释放,所以不能赋值
(integer) 0  


缓存击穿代码:

//   缓存穿透
//        Shop shop = queryPassThrough(id);

        //互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id);
        if(shop == null) {
            return Result.fail("店铺不存在!");
        }
        return Result.ok(shop);
    }

    public Shop queryWithMutex(Long id ){
        String key = CACHE_SHOP_KEY + id;
        Shop shop = null;
        String lockKey = "lock:shop:" + id;
        try {
//         1.从Redis中查询商铺的缓存
            String shopJson = stringRedisTemplate.opsForValue().get(key);
//         2.判断是否存在该商铺
            if (StrUtil.isNotBlank(shopJson)){  //只有字符串才是true
    //           存在就转成Bean并且返回
                return JSONUtil.toBean(shopJson,Shop.class);
            }
//     判断命中的是否是空值
            if(shopJson != null){
    //      缓存穿透
                return null;
            }

//        4.实现缓存重建
//        4.获取互斥锁

            boolean isLock = tryLock(lockKey); //尝试获取锁
            if (!isLock){  //获取锁失败
             Thread.sleep(50); //失败就睡眠,等待下次获取
                return queryWithMutex(id);
            }

//        3.不存在就从sql中查询
            shop = getById(id);
            if (shop == null){
    //          从数据库中查询不到该商铺就写入Redis一个空字符串
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
//        存在,写入Redis, 但要将实体类转化为JSON字符串
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw  new RuntimeException(e);
        } finally {
//            释放锁
            unlock(lockKey);
        }
        return shop;
    }

image-20220517163208488

image-20220517162511811

封装Redis工具类

image-20220517205701551

  • 封装的Redis工具类
@Slf4j
@Component
public class CacheClient {

     private final StringRedisTemplate stringRedisTemplate;

     public CacheClient(StringRedisTemplate stringRedisTemplate){
         this.stringRedisTemplate = stringRedisTemplate;
     }
     public void set(String key, Object value, Long time, TimeUnit timeUnit){
         stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value),time,timeUnit);
     }
    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit timeUnit){
//      设置逻辑过期时间
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(time)));
//        写入redis
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value));
    }

    //函数式编程 ID为参数的泛型,R为返回值的泛型
    //缓存穿透
    public <R,ID>R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R>dbFallback
                ,Long time,TimeUnit timeUnit){
        String key =keyPrefix + id;
//    从redis中查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
//       判断是否存在
         if(StrUtil.isNotBlank(json)){
             return  JSONUtil.toBean(json,type);
         }
//       判断是否为空字符串
        if(json != null){
//         不为null 表示为空字符串
            return null;  //返回null表示错误数据,不存在该店铺
        }
//     接下来查询mysql数据库
       R  r = dbFallback.apply(id);  //调用函数式编程中的方法
        if (r == null){  //不存在这个数据存入空字符串
            stringRedisTemplate.opsForValue().set(key,"",time,timeUnit);  //存入空字符串
            return null; //返回null表示数据错误
        }
//      存在就写入redis
        this.set(key,r,time,timeUnit);
        return r;
    }

}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值