Redis基础知识汇总

编程知识 更新时间:2023-05-01 18:38:18

Redis

作用:

1.基本命令

- get 	   //获得key的值

- set         //设置key的值

- exists       //是否存在该key,是的话则返回1,否则0

- expire      //给key设置过期时间

- TTl           //查看key的过期时间

- help       //查看命令的说明

- keys       //查看有哪些key

1.1 String

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5fr7YnKp-1668604587022)(.\photo\image-20221014165718176.png)]

- mset      //批量设置键值

- mget		//批量读取值

- INCR		//每次使用这个命令,都会使这个键的值+1

- INCRBY num 	//每次使用这个命令,都会使这个键的值+num,即自定义增加值

- SETNX			//设置key,如果已经存在同名key,创建失败

- SETEX			//设置key同时设置过期时间

1.2 Hash

这里面是field - value 的方式进行存储

Hash 和 String的区别,虽然String能存储类似的Json字符串,能达到像hash一样的存储数据,但是String所存储的是整个Json字符串,他里面有很多值,不

能对单一个值进行修改,只能整个字符串完整删除,重新写过,而hash能做到对单一值进行修改。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zCMZNwTS-1668604587023)(.\photo\image-20221012191329962.png)]

常用命令

-Hset key field value  //对key里面的field键存入value数据。(可以通过上面图进行理解)

-HGet key field

-Mset key field1 value1 field2 value....

-Mget key field1 field2....

-Hgetall

-Hkeys

-Hvals

-HINCRBY

-HSetNx

1.3 List

双向链表

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9Bt2I2Ht-1668604587023)(.\photo\image-20221014165630042.png)]

  • 删除插入快
  • 查询慢

List对于那些有顺序要求的比较适用(类似于朋友圈点赞顺序之类的)

-LPUSH key elem... //从左侧按顺序插入元素,可以一次性插入多个元素

-LPOP key num	//从左侧移除第num个元素

-RPUSH key elem		

-RPOP key

-LRANGE key star end		//查询key中第star到end范围内的元素

-BLPOP / -BRPOP key time	//进行 对key的元素查询,查询时间为time(即相当于一个阻塞程序,如果查询的key没有内容时,先不返回值,在给定时间内进行等
待,若在规定时间内有人往key插入了元素,则会返回该插入的元素)

1.4 Set

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dA8Uudvt-1668604587023)(.\photo\image-20221014165542994.png)]

  • 元素存储无序
  • 查询比较快
  • 元素不可以重复

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zSrg3yqL-1668604587024)(.\photo\image-20221014161340092.png)]

-SADD  key member....	//往set集合中的key中插入一个或者多个元素

-SREM	key member		//往set集合中的key移除一个或者多个元素

-SCARD	key 			//返回该key中的元素个数

-SMEMBER	key member	//判断一个元素是否在key中

-SMEMBERS	//获取set中的所有元素

-SINNER		key1 key2		//交集

-SDIFFF		key1 key2		//差集

-SUNION  	key1 key2		//并集

1.5 SortedSet

该集合的存储是value - score的方式存储,并且会自动根据score的大小进行排序

  • 可排序,插入元素时会自动排序(升序)
  • 元素不重复
  • 查询速度快

一般用于学生分数之类的

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-khzpo82k-1668604587024)(.\photo\image-20221014164642856.png)]

  • 默认情况下,他这里都是升序的,如果需要降序的话,在字母Z后面加上REV

例如:ZRANK是获取redis数据库中该元素在升序下的排名,ZREVRANK则是获取降序的元素排名

(还有个小的注意事项:即使在RDM可视化工具中是由1开始排序的,但是查询时的返回结果则是从0开始排序的,比如lucy是升序第5名,则她升序返回值是4)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vFqxzgp7-1668604587024)(.\photo\image-20221014165249418.png)]

2.Redis 连接工具

有点像JDBC

2.1 Jedis连接器

  • 好处:它里面的方法名和效果和redis中的命令一摸一样,比如set是设置string的键值对,jedis中的set方法也是一样的效果,比较容易上手

  • 缺点:他是单线程的,而且线程不安全,效率不高

 //jedis的操作示范类
private Jedis jedis;

    @BeforeEach
    void setUp(){
        //1.建立连接
        jedis= JedisUtils.getJedis();

        //输入密码(如果redis有密码的话)
        jedis.auth("137688");

        //选择在哪一个redis数据库进行插入(一般的redis都有16个数据库,编号0-15)
        jedis.select(0);
    }

    @Test
    void test(){
        String name=jedis.set("name","zhangsan");
        System.out.println(name);
        String name1 = jedis.get("name");
        System.err.println(name1);
    }


//Jedis的工具类
ublic class JedisUtils {

  	
    private static final JedisPool jedispool;

    //静态代码块,在执行程序前,优先执行这个代码块里面的内容
    static {
        //创建连接池
        JedisPoolConfig poolConfig =new JedisPoolConfig();

        //配置连接池
        //连接池最大存放连接对象数
        poolConfig.setMaxIdle(8);
        //设置最大空闲连接数
        poolConfig.setMaxTotal(8);
        //设置最小空闲连接数
        poolConfig.setMinIdle(0);
        
        //设置超时等待时间
        poolConfig.setMaxWaitMillis(1000);
        //配置密码端口那些
        jedispool =new JedisPool(poolConfig,
                "localhost",6379,1000,"137688");
    }

    //设置全局静态方法,别的类直接调用就能获取连接对象
    public static Jedis getJedis(){
        return jedispool.getResource();
    }
}

2.2 SpringDataRedis

spring所提供的一个类(里面封装了Redis的操作方法,整合了Jedis和lettuce,但是里面默认是使用Lettuce)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VaM6KY3x-1668604587025)(.\photo\image-20221015115634849.png)]

2.2.1 RedisTempate

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Sl0L9W8P-1668604587025)(.\photo\image-20221015121045076.png)]

//在容器中注入
@Resource
    private RedisTemplate redisTemplate;


//配置文件
  redis:
    host: localhost
    port: 6379
    password: 137688
    lettuce:
      pool:
        max-active: 8
        max-idle: 8
        min-idle: 0
        max-wait: 1000ms
            
  //demo          
 @Test
    void test08(){
        redisTemplate.opsForValue().set("name","mylover");
        System.out.println( redisTemplate.opsForValue().get("name") );
    }

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UbpMcar9-1668604587025)(.\photo\image-20221015151009969.png)]

  • RedisTemplate的默认序列化方式是JDK方式序列化,可以接收任意Object作为值写入Redis。其序列化后,导致数据库后面是这个样子

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VGYMZkME-1668604587026)(.\photo\image-20221015151644355.png)]

存入的key是name,序列化后则是 \xAC\xED\x00\x05t\x00\x04name

缺点 :

  • 可读性较差
  • 内存占用大

解决方式:

修改其序列化方式(默认JDK序列化)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3TDwfwOV-1668604587026)(.\photo\image-20221015154712925.png)]

写一个配置类在spring容器初始化之前实现对RedisTemplate的序列化进行指定,对key和value的序列化进行设置,这样即可完成配置

  • 使用JSON序列化存储value时的小问题

  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-FxXmmDFk-1668604587026)(.\photo\image-20221015155309390.png)]

  • 当value是java对象时,它不仅会把内容存进去,还会把class类型信息存入进去,这样会导致内存开销变大

解决方法:将value设置为String,自己手动进行进行JSON转String存入redis,取出来的时候也手动转回Json

2.2.2 StringRedisTemplate

这个类的keyvalue默认序列化方式是String

不过这种方式存入json需要自己手动序列化和反序列化

//注入bean
    @Resource
    private StringRedisTemplate stringRedisTemplate;

//json序列化工具类
    private static final ObjectMapper mapper=new ObjectMapper();
    @Test
    void test09() throws JsonProcessingException {
        //创建java对象类
        user user=new user(1,"zhangsan","123");
        //手动将java对象进行序列化
        String s = mapper.writeValueAsString(user);

        //在redis库中得到json
        stringRedisTemplate.opsForValue().set("user:1",s);
        System.out.println(stringRedisTemplate.opsForValue().get("user:1"));
        String user1 = stringRedisTemplate.opsForValue().get("user:1");
        //反序列化为java对象
        com.example.po.user user2 = mapper.readValue(user1, user.class);
        System.out.println(user2);
    }

这里补充一下json序列化方式:

  • 转json:使用ObjectMapper工具类中的 writeValueAsString方法,将Java对象序列化
  • json反序列化:使用readValue方法将json重新反序列化为java对象

2.3.数据结构的选择

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-T1h88O7N-1668604587027)(.\photo\image-20221017151538366.png)]

在保存用户信息的时候,最好使用hash,因为hash修改起来比较灵活,而且相对于string内存空间占用更少,因为string保存用户信息会使用json字符串保存,故

中间除了数据本身之外就会有一些冒号,引号之类的符号占据空间,所以相比之下,string会占据更多的内存。

小细节:1.因为只有像那些加了@Configuration,@controller等等那些标识类,交给spring管理的类,才能通过@resource或者其他注解来注入bean,此时如果想要弄bean的话,只能通过构造器注入了

package com.hmdp.utils;

import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

public class loginInterceptor implements HandlerInterceptor {

    //@Resource //由于这个类是我们自己创建的,且未交给spring管理,故需要构造器注入StringRedisTemplate
    private StringRedisTemplate stringRedisTemplate;

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

2.如果配置mvc拦截器的时候,想改变拦截器的执行顺序的话,可以使用order来控制(order越大的越后执行,order越小的月先执行)


@Configuration
public class mvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new loginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**").order(1);//后执行
        registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);//先执行
    }

}

3.短信验证


@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("手机号码错误");
        }

        //2.符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
     
        //3将验证码存入redis,并设置有效期2min
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //4.发送验证码
        log.debug("验证码发送成功,为{ " + code + " }");
        return Result.ok();
    }

    //登陆时,通过验证码验证手机号
    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        //1.先验证手机号码
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            //如果手机格式错误
            return Result.fail("手机号码错误");
        }

        //2.验证验证码是否正确
        //从redis中获取验证码,与用户输入的进行比对,验证
        String cachecode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if (loginForm.getCode() == null || !loginForm.getCode().equals(cachecode)) {
            return Result.fail("验证码错误");
        }

        //3.验证数据库中是否已经有该用户
        User user = query().eq("phone", phone).one();
        if (null == user) {
            //表示该用户没有创建账号,调用下面的创建函数,创建user
            user = creatUser(phone);
        }

        //4 生成token,将数据存入redis
        String token = UUID.randomUUID().toString();
        //java类对象的转换,利用工具类bean
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        //将Java类转化成map集合
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName,fieldValue) -> fieldValue.toString())) ;
        //存入redis
        stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY + token, userMap);
        //刷新redis过期时间
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok();
    }

    
    //创建函数
    public User creatUser(String phone) {
        User user = new User();
        //设置电话号码
        user.setPhone(phone);
        //生成随机名字
        String username = USER_NICK_NAME_PREFIX + RandomUtil.randomNumbers(10);
        user.setNickName(username);
        //往数据库中存入数据
        save(user);
        return user;
    }
}

工具类:

  • BeanUtils:里面有可以将有相同属性的Java对象进行互相转换,也可以将java对象转换成map集合之类的
  • UUID:可以生成随机数字,一半用于生成token和验证码

拦截器1:

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 javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class RefreshInterceptor implements HandlerInterceptor {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

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

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //这里一般是从请求头中获取token
        //String token = request.getHeader("authorization");
        String token ="xxxxxxxxxxxxx";
        if(StrUtil.isBlank(token)) {
            return true;
        }
        Map<Object, Object> user = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        if (user.isEmpty()) {
            return true;
        }
        UserDTO userDTO = BeanUtil.fillBeanWithMap(user, new UserDTO(), false);
        UserHolder.saveUser(userDTO);
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,LOGIN_USER_TTL, TimeUnit.MINUTES);
        return true;
    }
    
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

        UserHolder.removeUser();
    }
}

拦截器2:

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 javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

public class loginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        if(UserHolder.getUser()==null){
            response.setStatus(401);
            return false;
        }
                return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

       UserHolder.removeUser();
    }
}

配置拦截器:

package com.hmdp.config;

import com.hmdp.utils.RefreshInterceptor;
import com.hmdp.utils.loginInterceptor;
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;

import javax.annotation.Resource;

@Configuration
public class mvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new loginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**").order(1);
        registry.addInterceptor(new RefreshInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}

拦截器1和2互相配合使用,达到,既能拦截,又能更新token的效果

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-91I4Zq8S-1668604587027)(.\photo\image-20221017195750558.png)]

4.查询信息

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6R9iiW2D-1668604587027)(.\photo\image-20221017202939851.png)]

4.1缓存更新策略

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CNZGGHFr-1668604587027)(.\photo\image-20221017212714809.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jhY40y19-1668604587028)(.\photo\image-20221017213536419.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5diF0xk-1668604587028)(.\photo\image-20221017213651726.png)]

总结:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1qlIJ1vm-1668604587028)(.\photo\image-20221017213812831.png)]

4.2 缓存穿透

  • 当用户查询信息时,若是redis中没有的话,会继续查询数据库。此时如果有一些不怀好意的人,输入一些不存在的id(由于id不存在,自然redis中就不会有缓存数据),疯狂查询数据库,这样缓存就相当于一个旁观者,别人疯狂攻击数据库没数据库迟早会崩。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZJoTIVo7-1668604587029)(.\photo\image-20221017220632890.png)]

解决方式:

1.缓存空对象

​ 别人发送请求查询数据库,若是不存在,则设置往redis中该id设置一个null,这样就能比较简单的防止疯狂查询。

  • 优点:实现简单,维护方便

  • 缺点:额外占据内存消耗(因为无论存不存,你都会为这个id在redis中设置null

      	造成短期数据查询不一致(在设置null的这段时间中,要是真正有一个用户插入了该id的数据,而你此时的缓存是null,这样就会造成不一致)
    

(解决内存消耗的方法:设置null的同时,也为他设置一个比较短的过期时间)

2.布隆过滤

​ 在redis和数据库之前再加多一层屏障,其基于hash算法,将数据存的标识存储在一个字节数组中。

  • 优点:其占用的内存空间比较小,不会有多余的key
  • 缺点:实现复杂,而且也会存在误判的可能(就是标识说有数据,但redis和数据库中都没有数据)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mL5avskL-1668604587029)(.\photo\image-20221017221615548.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0RaMB8ed-1668604587029)(.\photo\image-20221018165812453.png)]

4.3缓存雪崩

  • 大量缓存同时失效,这样短时间内,会有很多请求直接查询数据库,短时间内数据库压力会增大
  • redis服务器直接宕机,导致每一个请求查询都弄到服务器上,导师服务器压力过大

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-D2edSjAH-1668604587029)(.\photo\image-20221018165934376.png)]

解决方案:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-a1XvDXMS-1668604587030)(.\photo\image-20221018170642771.png)]

4.4缓存击穿

  • 高并发访问 且 缓存重建业务复杂的key忽然失效了,导致无数请求瞬间给数据库带来巨大冲击

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DQf4wwqq-1668604587031)(.\photo\image-20221018171001857.png)]

​ 当有一个查询时,其查询到redis未有数据,其便去数据库中进行查询,由于这个查询是一个业务繁杂的查询(一些需要同时好几张表一起连表查询的数据,花

的时间会很久的那种),由于该查询时间花费较长 ,在查询时间中,其他线程也来查询这个数据,由于第一个线程还在查询数据库过程中,所以redis中不可能有

缓存,所以其它线程查询这个数据的时候,也是会前往数据库中查询,此时要是多个线程都来查询这个数据的话,都会直接前往数据库中查询,而且这个查询过程

还比较久,此时数据库就会有很大压力(多个复杂数据查询同时进行)。

解决方案:

  1. 上锁(互斥锁):即若是有一个查询开始,就给这个查询上个锁,其他线程后来的,想要查询,发现有锁,就会休眠等待然后再重新查询redis(或者其他操作),就会避免去查询数据库。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-F6mF8d6E-1668604587031)(/\photo\image-20221018172214257.png)]

  • **优点:**实现简单
  • **缺点:**过多线程请求查询时,由于没拿到锁,都会进行休眠等待,会降低性能;而且可能会死锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1M5ThGBH-1668604587031)(.\photo\image-20221018204117915.png)]

  1. 逻辑过期

    ​ 为每个高并发且查询复杂的 key 的 value 设置多一个字段(expire)表示是否已经过期,就会达到一种效果:虽然数据存在,但是他已经是一个过期了的数据。

    ​ 第一个查询的线程发现redis中没有数据,准备查询数据库,此时上锁,开启一个新的线程查询数据库(自己本身的线程不查询数据库),在开启新线程后,返回之前的过期旧数据。此时其他线程过来查询时,获取锁失败,不是休眠等待,而是直接返回旧数据,新开辟的查询线程在查询完数据之后,释放锁。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JhDZkgfO-1668604587032)(/\photo\image-20221018172940596.png)]

  • **优点:**线程无需等待,性能较好
  • **缺点:**需要额外的内存消耗
  1. 缓存击穿—互斥锁实现
  @Override
    public Result quertById(Long id) throws JsonProcessingException {
        //1.首先获取根据id,在redis中查询一下
        String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(s)) {
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return Result.ok(shop);
        }
        //
        System.out.println(s);
        if (s != null) {
            return Result.fail("没有查询到相关信息");
        }
        //要是redis里面没有缓存数据,就去数据库里面查询
        Shop shop = getById(id);
        //
        if (shop == null) {
            //这里表示数据库中也没有查询到相关的内容
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("没有查询到相关信息");
        }
        //
//        String json = mapper.writeValueAsString(shop);
        String json = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, json, 30, TimeUnit.MINUTES);
        return Result.ok(shop);

    }

改进版:

   @Override
    public Result quertById(Long id) throws JsonProcessingException {
        //解决缓存穿透
        //Shop shop = queryWithPassThrough(id);

        //解决缓存击穿---互斥锁
        Shop shop = queryWithMutex(id);
        if (shop == null) {
            return Result.fail("未查询到相关信息!");
        }
        return Result.ok(shop);

    }

    //解决缓存穿透
    public Shop queryWithMutex(Long id) {
        //1.首先获取根据id,在redis中查询一下
        String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        
        if (StrUtil.isNotBlank(s)) {
            //如果查询到信息的话,直接返回
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return shop;
        }
		//这个是为了防止缓存击穿,设置null值,防止疯狂查询数据库
        if (s != null) {
            return null;
        }
        
        //为该key设置锁id
        String lock = CACHE_SHOP_KEY + id;
        Shop shop = null;
        try {
            //获取互斥锁,这里是对该key在redis中设置一个值,利用setnx的特性,如果这个key的锁id在redis中有值的话,表示该key已经被上锁
            boolean tryLock = tryLock(lock);
            if (!tryLock) {
                //表示没有拿到锁,无法往下执行
                //休眠一会,给一个缓冲时间,然后重新尝试获取
                Thread.sleep(50);
                //重新尝试获取缓存数据
                return queryWithMutex(id);
            }
            
            //要是redis里面没有缓存数据,就去数据库里面查询
            shop = getById(id);
            Thread.sleep(200);  //模拟一下延迟,表示这一次查询花费时间较长
     
            if (shop == null) {
                //这里表示数据库中也没有查询到相关的内容
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            
            String json = JSONUtil.toJsonStr(shop);
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, json, 30, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException();
        } finally {
            //写入完数据之后,释放锁
            ulock(lock);
        }

        return shop;
    }

    //解决缓存击穿
    public Shop queryWithPassThrough(Long id) {
        //1.首先获取根据id,在redis中查询一下
        String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if (StrUtil.isNotBlank(s)) {
            Shop shop = JSONUtil.toBean(s, Shop.class);
            return shop;
        }
        //
        System.out.println(s);
        if (s != null) {
            return null;
        }
        //要是redis里面没有缓存数据,就去数据库里面查询
        Shop shop = getById(id);
        //
        if (shop == null) {
            //这里表示数据库中也没有查询到相关的内容
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            return null;
        }
        //
//        String json = mapper.writeValueAsString(shop);
        String json = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, json, 30, TimeUnit.MINUTES);
        return shop;
    }

    //上锁
    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, key + " 的锁", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    //解锁
    private void ulock(String key) {
        stringRedisTemplate.delete(key);
    }

setIfAbsentredis命令行中的setnx命令(这个命令表示如果这key在redis中已经存在的话就会返回0,且不能插入;若是不存在的话,可以插入key,而且返回1)

商品列表(shoptype)

@Override
    public Result queryTypeList() {
        String type = stringRedisTemplate.opsForValue().get(CACHE_SHOPTYPE_KEY);
//        System.out.println(type);
        if (StrUtil.isNotBlank(type)) {
            List<ShopType> typeList = JSONUtil.toList(type, ShopType.class);
            System.out.println(typeList);
            return Result.ok(typeList);
        }
        List<ShopType> typeList = typeService
                .query().orderByAsc("sort").list();
        if (typeList == null){
            return Result.fail("没有查询到相关信息");
        }
        System.out.println(typeList);
        type=JSONUtil.toJsonStr(typeList);
        stringRedisTemplate.opsForValue().set(CACHE_SHOPTYPE_KEY,type,30, TimeUnit.MINUTES);
        return Result.ok(typeList);
    }
  1. 缓存击穿—逻辑过期实现
 @Override
    public Result quertById(Long id) throws JsonProcessingException {
        //解决缓存击穿
//        Shop shop = queryWithPassThrough(id);

        //解决缓存穿透
//        Shop shop = queryWithMutex(id);
//        if (shop == null) {
//            return Result.fail("未查询到相关信息!");
//        }

        Shop shop = queryWithExpire(id);
        return Result.ok(shop);

    }

    //定义一个线程池
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

    //解决缓存击穿---逻辑过期
    public Shop queryWithExpire(Long id) {
        //1.首先获取根据id,在redis中查询一下
        String s = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);

        if (StrUtil.isBlank(s)) {
            //未命中直接返回
            return null;
        }

        RedisData redisData = JSONUtil.toBean(s, RedisData.class);
        JSONObject data = (JSONObject) redisData.getData();
        Shop shop = JSONUtil.toBean(data, Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        if (expireTime.isAfter(LocalDateTime.now())) {
            //表示未过期,直接返回数据
            return shop;
        }

        //到这,表示数据已经逻辑过期了,尝试获取互斥锁
        String lock = LOCK_SHOP_KEY + id;
        boolean b = tryLock(lock);
        if (b) {
            //获取锁成功,开启新线程,进行查询
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                //重建缓存
                try {
                    saveShopToRedis(id, 20L);
                } catch (InterruptedException e) {
                    throw new RuntimeException();
                }
                //释放锁
                ulock(lock);
            });
        }
        //获取锁失败,直接返回旧数据
        return shop;
    }

    //逻辑过期设置
    public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
        Shop shop = getById(id);
        Thread.sleep(200);
        RedisData redisData=new RedisData();
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
    }

到这,表示数据已经逻辑过期了,尝试获取互斥锁
String lock = LOCK_SHOP_KEY + id;
boolean b = tryLock(lock);
if (b) {
//获取锁成功,开启新线程,进行查询
CACHE_REBUILD_EXECUTOR.submit(() -> {
//重建缓存
try {
saveShopToRedis(id, 20L);
} catch (InterruptedException e) {
throw new RuntimeException();
}
//释放锁
ulock(lock);
});
}
//获取锁失败,直接返回旧数据
return shop;
}

//逻辑过期设置
public void saveShopToRedis(Long id, Long expireSeconds) throws InterruptedException {
    Shop shop = getById(id);
    Thread.sleep(200);
    RedisData redisData=new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(redisData));
}

更多推荐

Redis基础知识汇总

本文发布于:2023-04-23 18:15:00,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/4c447335a3fc918aeb387515e5b12041.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:基础知识   Redis

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!

  • 99264文章数
  • 25834阅读数
  • 0评论数