黑马Redis学习笔记 (基础篇+实战篇)

这篇具有很好参考价值的文章主要介绍了黑马Redis学习笔记 (基础篇+实战篇)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一.初始Redis

1.1SQL 和 NoSql的区别

1.1.1结构化和非结构化

(1) SQL关系性数据库

传统关系型数据库是结构化数据,每一张表都有严格的约束信息:字段名、字段数据类型、字段约束等等信息,插入的数据必须遵守这些约束

黑马Redis学习笔记 (基础篇+实战篇)

(2) NoSql数据库

NoSql对数据库格式没有严格约束,往往形式松散,自由。

可以是key-value,可以是文档,或者图格式

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

1.1.2关联和非关联

(1) 关系型数据库

黑马Redis学习笔记 (基础篇+实战篇)

(2) 非关系型数据库

{
  id: 1,
  name: "张三",
  orders: [
    {
       id: 1,
       item: {
	 id: 10, title: "荣耀6", price: 4999
       }
    },
    {
       id: 2,
       item: {
	 id: 20, title: "小米11", price: 3999
      
       }
    }
  ]
}

1.1.3查询方式

黑马Redis学习笔记 (基础篇+实战篇)

1.1.4 事务

传统关系型数据库能满足事务ACID的原则 ,而非关系型数据库往往不支持事务,或者不能严格保证ACID的特性,只能实现基本的一致性。

1.1.5总结

黑马Redis学习笔记 (基础篇+实战篇)

1.2 认识Redis

特征:

  • 键值(key-value)型,value支持多种不同数据结构,功能丰富

  • 单线程,每个命令具备原子性

  • 低延迟,速度快(基于内存、IO多路复用、良好的编码)。

  • 支持数据持久化(定期将内存搬运到磁盘)

  • 支持主从集群、分片集群(数据拆分)

  • 支持多语言客户端

Redis的官方网站地址:RedisRedis Redis

1.3 Redis安装启动

Redis是基于C编写,所以需要先安装Redis所需的gcc依赖

yum install -y gcc 

如果有了就跳过

安装包上传到usr/local/src

黑马Redis学习笔记 (基础篇+实战篇)

tar -zxvf redis-6.2.6.tar.gz 

cd到redis的目录:

cd redis-6.2.6/

编译和运行

make && make install

默认启动:

需要一直挂着页面

redis-server

后台启动:

前提是必须修改配置文件(/usr/local/src/redis-6.2.6/redis.conf)

(1) 先备份一份

cp redis.conf redis.conf.bak

(2) 修改

vi redis.conf

eg:修改密码为1234

黑马Redis学习笔记 (基础篇+实战篇)

(3) 运行

cd /usr/local/src/redis-6.2.6
redis-server redis.conf

(4) 查看是否启动

ps -ef | grep redis

黑马Redis学习笔记 (基础篇+实战篇)

(5) 停止redis -9:强制但是不安全

kill -9 进程号

开机自启:

(1) 新建系统服务文件

vi /etc/systemd/system/redis.service

内容:

[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/redis-server /usr/local/src/redis-6.2.6/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

(2) 重新加载系统服务

systemctl daemon-reload

(3)启动

systemctl start redis

(4)查看状态

systemctl status redis

黑马Redis学习笔记 (基础篇+实战篇)

(5) 设置开机自启

systemctl enable redis

黑马Redis学习笔记 (基础篇+实战篇)

1.4 Redis客户端

安装完成Redis,我们就可以操作Redis,实现数据的CRUD了。这需要用到Redis客户端,包括:

  • 命令行客户端
  • 图形化桌面客户端
  • 编程客户端
1.4.1.Redis命令行客户端

Redis安装完成后就自带了命令行客户端:redis-cli,使用方式如下:

redis-cli [options] [commonds]

redis-cli -h 192.168.200.131 -p 6379 -a 1234

其中常见的options有:

  • -h 127.0.0.1:指定要连接的redis节点的IP地址,默认是127.0.0.1
  • -p 6379:指定要连接的redis节点的端口,默认是6379
  • -a 123321:指定redis的访问密码

其中的commonds就是Redis的操作命令,例如:

  • ping:与redis服务端做心跳测试,服务端正常会返回pong

不指定commond时,会进入redis-cli的交互控制台:

黑马Redis学习笔记 (基础篇+实战篇)

也可以先不写密码,后面来补充!

黑马Redis学习笔记 (基础篇+实战篇)

1.4.2.图形化桌面客户端

GitHub上的大神编写了Redis的图形化桌面客户端,地址:https://github.com/uglide/RedisDesktopManager

不过该仓库提供的是RedisDesktopManager的源码,并未提供windows安装包。

在下面这个仓库可以找到安装包:https://github.com/lework/RedisDesktopManager-Windows/releases

resp.exe

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

二.Redis命令

黑马Redis学习笔记 (基础篇+实战篇)

expire设置存活周期,ttl查看剩余时间,不设置expire的话ttl为-1

1.String

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

setex key expireTime value

由于Redis为NoSql,我们不知道value对应的属性的数据类型是什么

黑马Redis学习笔记 (基础篇+实战篇)

如果Value是一个Java对象,例如一个User对象,则可以将对象序列化为JSON字符串后存储:

KEY VALUE
heima:user:1 {“id”:1, “name”: “Jack”, “age”: 21}
heima:product:1 {“id”:1, “name”: “小米11”, “price”: 4999}

一旦我们向redis采用这样的方式存储,那么在可视化界面中,redis会以层级结构来进行存储,形成类似于这样的结构,更加方便Redis获取数据**

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

2.Hash

本身Redis就是一个key-value的结构,而hash的value还是一个key-value的结构

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

支持对单个值进行修改

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.List

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

从左侧推

lpush users 1 2 3 

黑马Redis学习笔记 (基础篇+实战篇)

从右侧推

黑马Redis学习笔记 (基础篇+实战篇)

从左侧右侧弹出

lpop users 1
rpop users 1

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

阻塞弹出 blpop/brpop key second

blpop user1 100

4.Set

黑马Redis学习笔记 (基础篇+实战篇)

sadd s1 a b c

黑马Redis学习笔记 (基础篇+实战篇)

删除元素

 srem s1 a 

查看元素数量

scard s1 

多个集合之间的操作

sinsert s1 s2 //s1和s2的交集
sdiff s1 s2//s1和s2的差集
sunion s1 s2 //s1和s2的并集

黑马Redis学习笔记 (基础篇+实战篇)

sadd zs lisi wnagwu zhaoliu
sadd ls wangwu mazi ergou

scard zs
sinter zs ls
adiff zs ls
sunion zs ls
ismember zs lisi
ismember ls zhangsan
srem zs lisi

smembers zs
smembers ls

5.SortedSet

黑马Redis学习笔记 (基础篇+实战篇)

每个元素都带上分数,所以才能实现排序

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

zadd stus 85 jack 89 Lucy 82 Rose 95 Tom 78 Jerry 92 Amy 76 Miles 
zrem stus Tom 
zscore stus Amy
zrank stus Rose //升序 zrevrank stus Rose//降序
zcount stus 0 80
zincrby stus 2 Amy
zrange stus 0 2 //升序 zrevrange stus 0 2 //降序
zrangebyscore stus 0 80

三.Redis的java客户端

1.Jedis

在Redis官网中提供了各种语言的客户端,地址:https://redis.io/docs/clients/

基本用法

(1) 导入依赖

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.8.0</version>
</dependency>

(2) 建立连接

public class JedisTest {
    private Jedis jedis;

    @BeforeEach
    void setUp(){
        //1.建立连接
        jedis = new Jedis("192.168.200.130",6379);
        //2.设置密码
        jedis.auth("1234");
        //3.选择库
        jedis.select(0);
    }

    @Test
    void testString(){
        String result = jedis.set("name", "小明");
        System.out.println("result= " + result);

        String name = jedis.get("name");
        System.out.println("name= "+name);
    }

    @AfterEach
    void tearDown(){
        if(jedis!=null){
            jedis.close();
        }
    }


}

连接不上/报错 得使用设置密码

config set requirepass 12349

黑马Redis学习笔记 (基础篇+实战篇)

@Test
void testHash(){
    jedis.hset("user:1","name","jack");
    jedis.hset("user:1","age","21");

    Map<String, String> map = jedis.hgetAll("user:1");
    System.out.println(map);
}

黑马Redis学习笔记 (基础篇+实战篇)

Jedis连接池

建立Factory

public class JedisConnectFactory {
    private static final JedisPool jedisPool;

    static{
        //配置连接池
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxTotal(8);
        poolConfig.setMaxIdle(8);
        poolConfig.setMinIdle(0);
        poolConfig.setMaxWait(Duration.ofMillis(1000));
        jedisPool = new JedisPool(poolConfig,"192.168.200.130",6379,1000,"1234");
    }
    
    public static Jedis getJedis(){
        return jedisPool.getResource();
    }

}

代码说明:

  • 1) JedisConnectionFacotry:工厂设计模式是实际开发中非常常用的一种设计模式,我们可以使用工厂,去降低代的耦合,比如Spring中的Bean的创建,就用到了工厂设计模式

  • 2)静态代码块:随着类的加载而加载,确保只能执行一次,我们在加载当前工厂类的时候,就可以执行static的操作完成对 连接池的初始化

  • 3)最后提供返回连接池中连接的方法.

改造原始代码

代码说明:

1.我们在完成了使用工厂设计模式来完成代码的编写后,我们可以通过工厂来获得连接,不用去new对象,减低耦合,且使用的还是连接池对象

2.当我们使用连接池后,当我们关闭连接其实并不是关闭,而是将Jedis归还给连接池

@BeforeEach
void setUp(){
    //1.建立连接
    //jedis = new Jedis("192.168.200.130",6379);
    //2.设置密码
    //jedis.auth("1234");
    jedis = JedisConnectFactory.getJedis();
    //3.选择库
    jedis.select(0);
}

····

 @AfterEach
    void tearDown() {
        if (jedis != null) {
            jedis.close();
        }
    }

2.SpringDataRedis

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块就叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis

  • 提供了对不同Redis客户端的整合(Lettuce和Jedis)
  • 提供了RedisTemplate统一API来操作Redis
  • 支持Redis的发布订阅模型
  • 支持Redis哨兵和Redis集群
  • 支持基于Lettuce的响应式编程(Lettuce之前实在es那里有)
  • 支持基于JDK.JSON.字符串.Spring对象的数据序列化及反序列化
  • 支持基于Redis的JDKCollection实现

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同的类型中:

黑马Redis学习笔记 (基础篇+实战篇)

(1) 导入依赖

        <!--Redis依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--连接池依赖-->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

(2) 配置文件

spring:
  redis:
    host: 192.168.200.130
    port: 6379
    password: 1234
    database: 0
    lettuce:
      pool:
        max-active: 8 #最大连接数
        max-idle: 8 #最大空闲连接
        min-idle: 0 #最小空闲连接
        max-wait: 100 #连接等待时间

(3) 自定义的RedisTemplate,注入到IOC中

先去(4) 写测试类测试一下就会发现问题

由于我们使用的是String-Object的话,你value的值如果传入一个字符串或者其他类型的话,并没有序列化,就会造成以下情况:get key

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

这是由于key和value会被当成对象,被redis底层的默认序列化方法:jdk序列化工具jdkSerializationRedisSerialliszer

而它采用的是objectOutputStream(把java对象转成字节)

先把这个key删除掉,用del

写一个RedisConfig,写bean: 拥有我们的序列化后的redisTemplate

@Configuration

public class RedisConfig {
    @Bean
    public RedisTemplate<String,Object> redisTemplate( RedisConnectionFactory factory){
        RedisTemplate<String,Object> template = new RedisTemplate<>();
        template.setConnectionFactory(factory);

        // 设置key的序列化方式
        template.setKeySerializer(RedisSerializer.string());
        // 设置value的序列化方式
        template.setValueSerializer(RedisSerializer.json());
        // 设置hash的key的序列化方式
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置hash的value的序列化方式
        template.setHashValueSerializer(RedisSerializer.json());

        template.afterPropertiesSet();
        return template;
    }
}

(4) 测试类

@SpringBootTest
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;


    @Test
    public void RedistestString(){
        // 写入一条String数据
        redisTemplate.opsForValue().set("name", "wxp");
        // 获取string数据
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println("name = " + name);
    }
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

接下里试试实体类的序列化

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
   private String name;
   private Integer age;
}
@Test
public void testSaveUser(){
   User user = new User();
   user.setName("阿廖莎");
   user.setAge(21);
   redisTemplate.opsForValue().set("user:100",user);
   System.out.println(redisTemplate.opsForValue().get("user:100"));
}

控制台打印:

黑马Redis学习笔记 (基础篇+实战篇).

黑马Redis学习笔记 (基础篇+实战篇).

发现这个json对象会将类的class写入,这个是为这个class进行反序列化的,但是会存在内存开销

黑马Redis学习笔记 (基础篇+实战篇)

现在只需要用成StringRedisTemplate即可

@Autowired
private StringRedisTemplate stringRedisTemplate;

//JSON工具类ObjectMapper,或者可以用fastjson:JSON.toJSONString(), JSON.parseObject()
private static final ObjectMapper mapper = new ObjectMapper();

@Test
    public void testSaveUser() throws JsonProcessingException {
        User user = new User();
        user.setName("阿廖莎");
        user.setAge(21);
        //手动序列化
        String json = mapper.writeValueAsString(user);

        stringRedisTemplate.opsForValue().set("user:100",json);
        //反序列化
        User user1 = mapper.readValue(stringRedisTemplate.opsForValue().get("user:100"), User.class);
        System.out.println("user1 = " + user1);
    }

黑马Redis学习笔记 (基础篇+实战篇).

黑马Redis学习笔记 (基础篇+实战篇).

接下来测试下哈希结构

string:

@Test
void testHash(){
    stringRedisTemplate.opsForHash().put("user:200","name","张三");
    stringRedisTemplate.opsForHash().put("user:200","age","21");

    Object name = stringRedisTemplate.opsForHash().get("user:200", "name");
    System.out.println("属性name: " + name);
    Map<Object, Object> entries = stringRedisTemplate.opsForHash().entries("user:200");//获取全部的hashkey-hashvalue
    System.out.println("所有属性: " + entries);
}

黑马Redis学习笔记 (基础篇+实战篇).

黑马Redis学习笔记 (基础篇+实战篇).

实战篇====

四.Redis实战

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

一.短信登录

黑马Redis学习笔记 (基础篇+实战篇)

1.1导入hmdp项目

(1) 数据库

少数据的话,用navicat创建数据库hmdp,用idea连接mysql执行sql,用的还是本地的数据库

黑马Redis学习笔记 (基础篇+实战篇)

(2) 导入半成品hm-dianping

黑马Redis学习笔记 (基础篇+实战篇)

(3) 导入前端的话就导入nginx的文件夹,内部有hmdp的前端资源,我们导入后启动它即可

nginx目录下cmd输入start nginx.exe

访问:http://localhost:8080/

1.2session实现短信登录

黑马Redis学习笔记 (基础篇+实战篇)

(1) userService创建sendCode接口,实现它

@Override
public Result sendCode(String phone, HttpSession session) {
    //1.校验手机号:利用util下RegexUtils进行正则验证
    if(RegexUtils.isPhoneInvalid(phone)){
        return Result.fail("手机号格式不正确!");
    }
    //2.生成验证码:导入hutool依赖,内有RandomUtil
    String code = RandomUtil.randomNumbers(6);
    //3.保存验证码到session
    session.setAttribute("code",code);
    //4.发送验证码
    log.info("验证码为: " + code);
    log.debug("发送短信验证码成功!");

    return Result.ok();

}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

(2) login功能

这个老师的写法也是存在问题:假如我先用自己的获取验证码,再换别人的手机号用我的验证码登录,也可以登录

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    //1.校验手机号
    String phone = loginForm.getPhone();
    if(RegexUtils.isPhoneInvalid(phone)){
     return Result.fail("手机号格式错误!");
    }
    //2.校验验证码
    Object cacheCode = session.getAttribute("code");
    String code = loginForm.getCode();
    if(code==null||!cacheCode.toString().equals(code)){
        //3.不一致,报错
        return Result.fail("验证码错误!");
    }
     //4.一致,根据手机号查询用户(需要写对应的单表查询方法:select * from tb_user where phone = #{phone})
    User user = query().eq("phone", phone).one();
    if(user==null){
        //5.注册用户
        user.setPhone(phone);
        user.setNickName("user_"+RandomUtil.randomString(10));
        //保存用户
        save(user);
    }
    //6.存入session
    session.setAttribute("user",user);
    return Result.ok();
}

黑马Redis学习笔记 (基础篇+实战篇)

(3) 登录验证功能

其实就是携带登录配置,这里用的是cookie,然后用拦截器进行拦截然后验证,但是一般我们用jwt令牌放入localstroagecookie

上面我们不能直接把user存入,而是要把保留一些不隐私的信息(UserDto)然后传入Session

a.

//6.存入session
session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));

b.拦截器

public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1.获取session
        HttpSession session = request.getSession();
        //2.获取session中的用户
        Object user = session.getAttribute("user");
        //3.判断用户是否存在. 不存在:拦截;存在:放入ThreadLocal,放行(写了ThreadLocal的封装工具类UserHolder)
        if(user==null){
            response.setStatus(401);
            response.getWriter().write("用户未登录!");
            return false;
        }
        UserHolder.saveUser((UserDTO) user);
        return true;
    }

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

c.在MvcConfig内添加上我们的拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

黑马Redis学习笔记 (基础篇+实战篇)

1.3集群的session共享问题

多台Tomcat不共享session存储空间,当请求切换到不同的tomcat服务时导致数据丢失的问题

所以我们把数据存入Redis,集群的Redis可以替代session

1.4基于Redis实现共享session登录

我们应该选择String类型存验证码即可,value:验证码,但是key要区分开来

选择Hash存储用户信息,因为每个字段独立,比较好去DRUD,内存占用少,key用token即可(随机字符串)

之前的session的话,tomcat会自动把session的Id存入Cookie,每次请求都会携带Cookie,所以我们需要手动把token返回给客户端,每次请求客户端都会携带着token

黑马Redis学习笔记 (基础篇+实战篇)

基于上面的来进行修改

RedisConstants工具类存储key常量

public class RedisConstants {
    public static final String LOGIN_CODE_KEY = "login:code:";
    public static final Long LOGIN_CODE_TTL = 2L;
    public static final String LOGIN_USER_KEY = "login:token:";
    public static final Long LOGIN_USER_TTL = 36000L;

    public static final Long CACHE_NULL_TTL = 2L;

    public static final Long CACHE_SHOP_TTL = 30L;
    public static final String CACHE_SHOP_KEY = "cache:shop:";

    public static final String LOCK_SHOP_KEY = "lock:shop:";
    public static final Long LOCK_SHOP_TTL = 10L;

    public static final String SECKILL_STOCK_KEY = "seckill:stock:";
    public static final String BLOG_LIKED_KEY = "blog:liked:";
    public static final String FEED_KEY = "feed:";
    public static final String SHOP_GEO_KEY = "shop:geo:";
    public static final String USER_SIGN_KEY = "sign:";
}

sendCode做以下修改

//3.保存验证码到Redis
stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//有效期2mins

login

UserServiceImpl的sendCode和Login

@Service
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号:利用util下RegexUtils进行正则验证
        if(RegexUtils.isPhoneInvalid(phone)){
            return Result.fail("手机号格式不正确!");
        }
        //2.生成验证码:导入hutool依赖,内有RandomUtil
        String code = RandomUtil.randomNumbers(6);
        //3.保存验证码到Redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY +phone,code,LOGIN_CODE_TTL, TimeUnit.MINUTES);//有效期2mins
        //4.发送验证码
        log.info("验证码为: " + code);
        log.debug("发送短信验证码成功!");

        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);
        String code = loginForm.getCode();
        if(cacheCode==null||!cacheCode.equals(code)){
            //3.不一致,报错
            return Result.fail("验证码错误!");
        }
         //4.一致,根据手机号查询用户(需要写对应的单表查询方法:select * from tb_user where phone = #{phone})
        User user = query().eq("phone", phone).one();
        if(user==null){
            //5.注册用户
            User newUser = new User();
            newUser.setPhone(phone);
            newUser.setNickName("user_"+RandomUtil.randomString(10));
            save(newUser);
            user = newUser;
        }
        //6.保存用户到Redis
            //(1)生成token
            String token = UUID.randomUUID().toString(true);//hutools
            //(2)User转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user,UserDTO.class);
        HashMap<Object, Object> userMap = new HashMap<>();
        userMap.put("id", userDTO.getId().toString());
        userMap.put("nickName", userDTO.getNickName());
        userMap.put("icon", userDTO.getIcon());
            //(3)存储到Redis
            String tokenKey = LOGIN_USER_KEY + token;
            stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);

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

        return Result.ok(token);
    }
}

MvcConfig注入stringRedisTemplate,然后传给LoginInterceptor,因为LoginInterceptor不是bean不能用spring注入其他bean

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .excludePathPatterns(
                        "/user/code",
                        "/user/login",
                        "/blog/hot",
                        "/shop/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/voucher/**"
                );
    }
}

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor{

    private final StringRedisTemplate stringRedisTemplate;

    public LoginInterceptor(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)){
            //不存在,拦截 设置响应状态吗为401(未授权)
            response.setStatus(401);
            return false;
        }
        //2.基于token获取redis中用户
        String key=RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if (userMap.isEmpty()){
            //4.不存在则拦截,设置响应状态吗为401(未授权)
            response.setStatus(401);
            return false;
        }
        //5.将查询到的Hash数据转化为UserDTO对象
        UserDTO userDTO=new UserDTO();
        BeanUtil.fillBeanWithMap(userMap,userDTO, false);
        //6.保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.SECONDS);
        //8.放行
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //销毁,以免内存泄漏
        UserHolder.removeUser();
    }
}

用户请求进去拦截器,我们试着去获取请求头内的token,根据token去查询用户信息,判断是否拦截,保存在ThreadLocal,刷新token的有效期

但是,这个拦截器是拦截需要登录之后才需要进行请求的路径,那我如果一直在访问的是不需要拦截的页面的话,我还是会过期?这就不合理。所以我们需要在这个拦截器前面再加个拦截器,然后在新增拦截器上进行保存ThreadLocal和刷新有效期,不理解其他

其实就是对之前的拦截器进行功能拆分

黑马Redis学习笔记 (基础篇+实战篇)

MvcConfig

@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);//RefreshTokenInterceptor 先于 LoginInterceptor 执行
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).order(0);//默认拦截所有请求
    }

}

RefreshTokenInterceptor

public class RefreshTokenInterceptor implements HandlerInterceptor{

    private final 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)){
            return true;
        }
        //2.基于token获取redis中用户
        String key=RedisConstants.LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        //3.判断用户是否存在
        if (userMap.isEmpty()){
            return true;
        }
        //5.将查询到的Hash数据转化为UserDTO对象
        UserDTO userDTO=new UserDTO();
        BeanUtil.fillBeanWithMap(userMap,userDTO, false);
        //6.保存用户信息到ThreadLocal
        UserHolder.saveUser(userDTO);
        //7.更新token的有效时间,只要用户还在访问我们就需要更新token的存活时间
        stringRedisTemplate.expire(key, RedisConstants.LOGIN_USER_TTL, TimeUnit.MINUTES);
        //8.放行
        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //销毁,以免内存泄漏
        UserHolder.removeUser();
    }
}

LoginInterceptor

public class LoginInterceptor implements HandlerInterceptor{

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断是否需要拦截
        if(UserHolder.getUser()==null){
            response.setStatus(401);
            response.getWriter().write("用户未登录!");
            return false;
        }

        return true;
    }


    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //销毁,以免内存泄漏
        UserHolder.removeUser();
    }
}

黑马Redis学习笔记 (基础篇+实战篇)

刷新以下首页

黑马Redis学习笔记 (基础篇+实战篇)

二.商户查询缓存

黑马Redis学习笔记 (基础篇+实战篇)

2.1什么是缓存

黑马Redis学习笔记 (基础篇+实战篇)

数据库发生改变,Redis还没及时更新,那么从缓存内取到的数据就会出错,就是数据一致性问题

2.2添加商户缓存

黑马Redis学习笔记 (基础篇+实战篇)

我们通过这个接口查询到的数据有很多,我们希望在此做个Redis缓存数据,提供查询速度

黑马Redis学习笔记 (基础篇+实战篇)

(1)service定义方法,实现它

@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result querygetById(Long id) {
        //1.从Redis内查询商品缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        if(StrUtil.isNotBlank(shopJson)){
            //手动反序列化
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //2.不存在就根据id查询数据库
        Shop shop = getById(id);
        if(shop==null){
            return Result.fail("商户不存在!");
        }
        //3.数据库数据写入Redis
        //手动序列化
        String shopStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }
}

controller

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    return shopService.querygetById(id);
}

黑马Redis学习笔记 (基础篇+实战篇)

可以通过调试查看是否是从Redis内拿出来

时间变快了很多

黑马Redis学习笔记 (基础篇+实战篇)

2.3 添加商户类型缓存

作业:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result queryList() {
        //1.从Redis中查询
        String key = CACHE_SHOPTYPE_KEY;
        List<String> list = stringRedisTemplate.opsForList().range(key, 0, -1);
        if(!list.isEmpty()){
            //手动反序列化
            List<ShopType> typeList = new ArrayList<>();
            for (String s : list) {
                ShopType shopType = JSONUtil.toBean(s, ShopType.class);
                typeList.add(shopType);
            }
            return Result.ok(typeList);
        }

        //2.从数据库内查询
        List<ShopType> typeList = query().orderByAsc("sort").list();
        if(typeList.isEmpty()){
            return Result.fail("不存在该分类!");
        }
          //序列化
        for (ShopType shopType : typeList) {
            String s = JSONUtil.toJsonStr(shopType);
            list.add(s);
        }

        //3.存入缓存
        stringRedisTemplate.opsForList().rightPushAll(key,list);
        stringRedisTemplate.expire(key,CACHE_SHOPTYPE_TTL,TimeUnit.MINUTES);
        return Result.ok(list);
    }
}

黑马Redis学习笔记 (基础篇+实战篇)

2.4缓存更新策略

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

由于数据库的操作速度比操作缓存的速度慢,所以操作缓存的时候极低概率会被操作数据库的线程抢去cpu,反过来就会出现线程安全问题,所以采用先更新数据库再删除缓存

黑马Redis学习笔记 (基础篇+实战篇)

2.5实现商铺缓存和数据库的双写一致

黑马Redis学习笔记 (基础篇+实战篇)

ShopServiceImpl

@Override
public Result update(Shop shop) {
    if(shop.getId()==null){
        return Result.fail("店铺id不能为空!");
    }
    //1.更新数据库
    updateById(shop);
    //2.删除缓存
    String key = CACHE_SHOP_KEY + shop.getId();
    stringRedisTemplate.delete(key);
    return Result.ok();
}

ShopController

@PutMapping
public Result updateShop(@RequestBody Shop shop) {
    // 写入数据库
    return shopService.update(shop);
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

后面再访问的时候才会重新添加上缓存,这个之前就写过了

重新刷新

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

2.6缓存穿透的解决思路

避免数据库也查不到,还把null存入缓存,那么以后缓存就永远不生效

黑马Redis学习笔记 (基础篇+实战篇)

2.7解决商铺查询的缓存问题

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

2.8缓存雪崩

黑马Redis学习笔记 (基础篇+实战篇)

解决方案:

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

2.9缓存击穿

缓存击穿问题,也叫 热点 Key 问题;就是一个被 高并发访问 并且 缓存中业务较复杂的 Key 突然失效,大量的请求在极短的时间内一起请求这个 Key 并且都未命中,无数的请求访问在瞬间打到数据库上,给数据库带来巨大的冲击。

黑马Redis学习笔记 (基础篇+实战篇)

解决方案:

  • 互斥锁:查询缓存未命中,获取互斥锁,获取到互斥锁的才能查询数据库重建缓存,将数据写入缓存中后,释放锁。
  • 逻辑过期:查询缓存,发现逻辑时间已经过期,获取互斥锁,开启新线程;在新线程中查询数据库重建缓存,将数据写入缓存中后,释放锁;在释放锁之前,查询该数据时,都会将过期的数据返回。

黑马Redis学习笔记 (基础篇+实战篇)

解决方案 优点 缺点
互斥锁 没有额外的内存消耗;保证一致性;实现简单 线程需要等待,性能受影响;可能有死锁风险
逻辑过期 线程无需等待,性能较好 有额外内存消耗;不保证一致性;实现复杂

基于互斥锁解决缓存击穿问题

黑马Redis学习笔记 (基础篇+实战篇)

核心:利用 Redis 的 setnx 方法来表示获取锁。该方法的含义是:如果 Redis 中没有这个 Key,则插入成功;如果有这个 Key,则插入失败。通过插入成功或失败来表示是否有线程插入 Key,插入成功的 Key 则认为是获取到锁的线程;释放锁就是将这个 Key 删除,因为删除 Key 以后其他线程才能再执行 setnx 方法。

stenx lock 1

/**
 * 获取互斥锁
 */
private boolean tryLock(String key) {
    Boolean flag = redisTemplate.opsForValue().setIfAbsent(key, "1", TTL_TEN, TimeUnit.SECONDS);
    return BooleanUtil.isTrue(flag);
}

/**
 * 释放互斥锁
 */
private void unLock(String key) {
    redisTemplate.delete(key);
}

一次请求的过程

  1. 请求打进来,先去 Redis 中查,未命中;

  2. 获取互斥锁:将一个 Key 为 LOCK_SHOP_KEY + id 的数据写入 Redis 中,此时其他线程就无法拿到这个 Key,也就无法继续后续操作;

  3. 获取失败就进行休眠,休眠结束后通过递归再次请求;

  4. 获取成功,查询数据库、将需要查询的那个数据写入 Redis;

  5. 最后,删除通过 setnx 创建的那个 Key。

需求:修改根据id查询店铺的业务(互斥锁方式解决)

/**互斥锁实现解决缓存击穿**/
public Shop queryWithMutex(Long id){
    //1.从Redis内查询商品缓存
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    if(StrUtil.isNotBlank(shopJson)){
        //手动反序列化
        return JSONUtil.toBean(shopJson, Shop.class);
    }
    //如果上面的判断不对,那么就是我们设置的""(有缓存"",证明数据库内肯定是没有的)或者null(没有缓存)
    //判断命中的是否时空值
    if(shopJson!=null){//
        return null;
    }

    //a.实现缓存重建
    //a.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    Shop shop = null;
    try {
        boolean hasLock = tryLock(lockKey);
        //a.2 判断是否获取到,获取到:根据id查数据库 获取不到:休眠
        if(!hasLock){
            Thread.sleep(50);
            return queryWithMutex(id);
        }

        //2.不存在就根据id查询数据库
        shop = getById(id);
        //模拟重建的延时
        Thread.sleep(200);
        if(shop==null){
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return null;
        }
        //3.数据库数据写入Redis
        //手动序列化
        String shopStr = JSONUtil.toJsonStr(shop);
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,shopStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
    } catch (InterruptedException e) {
        throw new RuntimeException(e);
    } finally {
        //释放互斥锁
        unlock(lockKey);
    }

    return shop;
}
@Override
public Result querygetById(Long id) {
    //缓存穿透
    //Shop shop = queryWithPassThrough(id);

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

    return Result.ok(shop);
}

利用postman测试多线程:先把这个key的缓存删除

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

访问1000次,数据库只查询一次,都可以200,说明互斥锁设置后效果成功

基于逻辑过期解决缓存击穿问题

黑马Redis学习笔记 (基础篇+实战篇)

private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//开启10个线程

/**逻辑过期实现解决缓存击穿**/
public Shop queryWithLogical(Long id){
    //1.从Redis内查询商品缓存
    String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
    //2.判断是否存在
    if(StrUtil.isBlank(shopJson)){
        return null;
    }
    //3.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject data = (JSONObject) redisData.getData();
    Shop shop = JSONUtil.toBean(data, Shop.class);

    //4.判断是否过期
    LocalDateTime expireTime = redisData.getExpireTime();
    if(expireTime.isAfter(LocalDateTime.now())){
        //未过期直接返回
        return shop;
    }
        //5.过期的话需要缓存重建
    //5.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean hasLock = tryLock(lockKey);
    //5.2判断是否获取到,获取到:根据id查数据库 获取不到:休眠
   if(hasLock){
       //成功就开启独立线程,实现缓存重建, 这里的话用线程池
       CACHE_REBUILD_EXECUTOR.submit(()->{
           try {
               //重建缓存
               this.saveShop2Redis(id,20L);
           } catch (Exception e) {
               throw new RuntimeException(e);
           }finally {
               //释放锁
               unlock(lockKey);
           }

       });

   }

    return shop;
}
/**缓存重建方法**/
public void saveShop2Redis(Long id,Long expireSeconds) throws InterruptedException {
    //1.查询店铺信息
    Shop shop = getById(id);
    Thread.sleep(200);
    //2.封装逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(shop);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
    //3.写入Redis
    stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id,JSONUtil.toJsonStr(redisData));
}

先用saveShop2Redis把热点key提前放入缓存,提升速度(设置了逻辑过期时间)

@Test
void testSaveShop() throws InterruptedException {
    shopService.saveShop2Redis(1L,10L);
}ja'v

黑马Redis学习笔记 (基础篇+实战篇)

把数据库修改一下

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

大约200ms后就会进行缓存重建

黑马Redis学习笔记 (基础篇+实战篇)

2.10 封装Redis工具类

黑马Redis学习笔记 (基础篇+实战篇)

方法3:

id2 -> getById(id2) 在java8是可以用 this::getById 代替的
/**解决缓存穿透**/
public <R,ID> R queryWithPassThrough(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(CACHE_SHOP_KEY + id);
    if(StrUtil.isNotBlank(json)){
        //手动反序列化
        return JSONUtil.toBean(json, type);
    }
    //如果上面的判断不对,那么就是我们设置的""(有缓存"",证明数据库内肯定是没有的)或者null(没有缓存)
    //判断命中的是否时空值
    if(json!=null){//
        return null;
    }
    //2.不存在就根据id查询数据库
    R r = dbFallBack.apply(id);//由于不知道这段逻辑,所以我们需要用户传进来函数逻辑
    if(r==null){
        stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
        return null;
    }
    //写入Redis
    this.set(key,r,time,unit);
    return r;
}
@Override
public Result querygetById(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("店铺不存在!");

    return Result.ok(shop);
}

测试:数据库访问一次,传入Redis为“”,多刷后不会查询到数据库

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

方法4:

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

/**逻辑过期实现解决缓存击穿**/
public <R,ID> R queryWithLogical(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time,TimeUnit unit) {
    String key = CACHE_SHOP_KEY + id;
    //1.从Redis内查询商品缓存
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2.判断是否存在
    if (StrUtil.isBlank(shopJson)) {
        return null;
    }
    //3.命中,需要先把json反序列化为对象
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    JSONObject data = (JSONObject) redisData.getData();
    R r = JSONUtil.toBean(data, type);

    //4.判断是否过期
    LocalDateTime expireTime = redisData.getExpireTime();
    if (expireTime.isAfter(LocalDateTime.now())) {
        //未过期直接返回
        return r;
    }
    //5.过期的话需要缓存重建
    //5.1 获取互斥锁
    String lockKey = LOCK_SHOP_KEY + id;
    boolean hasLock = tryLock(lockKey);
    //5.2判断是否获取到,获取到:根据id查数据库 获取不到:休眠
    if (hasLock) {
        //成功就开启独立线程,实现缓存重建, 这里的话用线程池
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重建缓存(查数据库+传入Redis)
                R r1 = dbFallBack.apply(id);
                this.setWithLogicalExpire(key,r1,time,unit);
            } catch (Exception e) {
                throw new RuntimeException(e);
            } finally {
                //释放锁
                unlock(lockKey);
            }
        });
    }
    return r;
}

//设置锁
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);
}
@Override
public Result querygetById(Long id) {
    //解决缓存穿透
    //Shop shop = cacheClient.queryWithPassThrough(CACHE_SHOP_KEY,id,Shop.class,this::getById,CACHE_SHOP_TTL,TimeUnit.MINUTES);

    //互斥锁解决缓存击穿
    //Shop shop = queryWithMutex(id);

    //逻辑过期解决缓存击穿
    Shop shop = cacheClient.queryWithLogical(CACHE_SHOP_KEY,id,Shop.class,this::getById,20L,TimeUnit.SECONDS);//方便测试
    if(shop==null) return Result.fail("店铺不存在!");

    return Result.ok(shop);
}

测试:

我们先在Redis内插入逻辑过期时间的key

public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit){
    //逻辑过期时间 redisData有属性expireTime和Data,把value封装到里面就有了逻辑过期时间
    RedisData redisData = new RedisData();
    redisData.setData(value);
    redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
    //写入Redis
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));
}
@Test
void testSaveShop() throws InterruptedException {
    Shop shop = shopService.getById(1L);
    cacheClient.setWithLogicalExpire(CACHE_SHOP_KEY,shop,1L, TimeUnit.SECONDS);
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

postman发送多次请求

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

三.优惠卷秒杀

3.1全局唯一ID

tb_voucher_order表

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

在分布式系统下生成全局唯一ID的工具,满足 唯一性,高可用,高性能,递增性,安全性

这里利用的是Redis自增id策略

为了增加ID的安全性,不要直接使用Redis自增的数值,而是拼接一些其它信息:

黑马Redis学习笔记 (基础篇+实战篇)

一秒接收2^32,完全够用

ID的组成部分:符号位:1bit,永远为0

时间戳:31bit,以秒为单位,可以使用69年

序列号:32bit,秒内的计数器,支持每秒产生2^32个不同ID

@Component
public class RedisIdWorker {
    //到今年第一天的秒数
    private static final long BEGIN_TIMESTAMP = 1640995200L;

    //序列号的位数
    private static final long COUNT_TIMESTAMP = 32L;

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    public long nextId(String keyPrefix){//不同业务

        //生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = nowSecond-BEGIN_TIMESTAMP;

        //生成序列号
        String today = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("irc:" + keyPrefix + ":" + today);

        //拼接
        return timeStamp << COUNT_TIMESTAMP  |  count ;

    }

    public static void main(String[] args) {
        LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
        long second = time.toEpochSecond(ZoneOffset.UTC);
        System.out.println("second = " + second);
    }
}

测试:

@Resource
private RedisIdWorker redisIdWorker;

private ExecutorService es = Executors.newFixedThreadPool(500);//线程池

@Test
void testRedisId() throws InterruptedException {
    //CountDownLatch大致的原理是将任务切分为N个,让N个子线程执行,并且有一个计数器也设置为N,哪个子线程完成了就N-1
    CountDownLatch latch = new CountDownLatch(300);

    Runnable task =()->{
        for(int i=0;i<100;i++){
            Long id = redisIdWorker.nextId("order");
            System.out.println("id = " + id);
        }
        latch.countDown();
    };
    Long begin = System.currentTimeMillis();
    for(int i=0;i<300;i++){
        es.submit(task);
    }
    latch.await();
    Long end = System.currentTimeMillis();
    System.out.println("time = " + (end - begin));
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.2添加优惠卷

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

类似拓展

黑马Redis学习笔记 (基础篇+实战篇)

{
    "shopId":1,
    "title":"100元代金券",
    "subTitle":"周一至周五均可使用",
    "rules":"全场通用\n无需预约\n可无限叠加\n不兑现、不找零\n仅限堂食",
    "payValue":8000,
    "actualValue":10000,
    "type":1,
    "stock":100,
    "beginTime":"2022-11-13T10:09:17",
    "endTime":"2022-11-13T22:10:17"
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

点击抢购:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.3实现秒杀下单

黑马Redis学习笔记 (基础篇+实战篇)

@Override
@Transactional
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始,是否结束
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    if(voucher.getEndTime().isBefore(LocalDateTime.now())){
        return Result.fail("秒杀已结束!");
    }
    //3.判断库存是否充足
    if(voucher.getStock()<=0){
        return Result.fail("优惠券库存不足!");
    }
    //4.扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock -1")
            .eq("voucher_id", voucherId).update();
    //5.创建订单
    if(!success){
        return Result.fail("优惠券库存不足!");
    }
    //6.返回订单id
    VoucherOrder voucherOrder = new VoucherOrder();
    //6.1订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //6.2用户id
    Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    //6.3代金券id
    voucherOrder.setVoucherId(voucherId);

    //7.订单写入数据库
    save(voucherOrder);
    
    //8.返回订单Id
    return Result.ok(orderId);
}

测试:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

但是存在很多问题,多线程问题,单用户抢多张文图

3.4超卖现象

测试:

黑马Redis学习笔记 (基础篇+实战篇)类似postman携带header

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.5超卖问题分析

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

悲观锁比较简单,直接加锁即可,乐观锁难在判断

第一种:携带另一个变量进行判断

黑马Redis学习笔记 (基础篇+实战篇)

第二种:用数据本身有没有变化进行判断

黑马Redis学习笔记 (基础篇+实战篇)

3.6乐观锁解决超卖问题

//4.扣减库存
boolean success = seckillVoucherService.update()
        .setSql("stock = stock -1")
        .eq("voucher_id", voucherId).eq("stock",voucher.getStock())//where id = ? and stock =? 添加了乐观锁
        .update();

黑马Redis学习笔记 (基础篇+实战篇)

结果是不会出现线程安全问题,但是优惠券会出现过剩的情况,这就是乐观所的弊端:例如多个线程一开始标识stock为100,然后有个线程把stock减一了,其他那些线程就会返回错误

改进:

不去判断库存是否改变,判断库存>0即可

//4.扣减库存
boolean success = seckillVoucherService.update()
        .setSql("stock = stock -1")
        .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock >0 添加了乐观锁
        .update();

黑马Redis学习笔记 (基础篇+实战篇)

3.7实现一人一单功能

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

//查询订单看看是否存在
Long userId = UserHolder.getUser().getId();
if (query().eq("user_id",userId).eq("voucher_id",voucherId).count()>0) {
    return Result.fail("用户已经购买过一次!");
}

但是你要加锁,不让遇到多线程还是会下多个单,所以需要改进:没法判断这个数据是否修改过,因为一开始不存在,不能用乐观锁,所以只能用乐观锁

问题:能否用乐观锁执行?

不能,原因是乐观锁只能操作单个变量,而创建订单需要操作数据库

(1) 对实现类进行修改

@Override
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始,是否结束
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    if(voucher.getEndTime().isBefore(LocalDateTime.now())){
        return Result.fail("秒杀已结束!");
    }
    //3.判断库存是否充足
    if(voucher.getStock()<=0){
        return Result.fail("优惠券库存不足!");
    }

    Long userId = UserHolder.getUser().getId();
    synchronized (userId.toString().intern()) {//userId一样的持有同一把锁,最好不要放在整个方法上,intern:去字符串常量池找相同字符串
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//获得代理对象
        return proxy.createVoucherOrder(voucherId);//默认是this,我们要实现事务需要proxy
    }//先获取锁,然后再进入方法,确保我的前一个订单会添加上,能先提交事务再释放锁
}

@Transactional
public Result createVoucherOrder(Long voucherId){
    //查询订单看看是否存在
    Long userId = UserHolder.getUser().getId();

    if (query().eq("user_id",userId).eq("voucher_id",voucherId).count()>0) {
        return Result.fail("用户已经购买过一次!");
    }

    //4.扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock -1")
            .eq("voucher_id", voucherId).gt("stock",0)//where id = ? and stock >0 添加了乐观锁
            .update();
    //5.创建订单
    if(!success){
        return Result.fail("优惠券库存不足!");
    }

    //6.返回订单id
    VoucherOrder voucherOrder = new VoucherOrder();
    //6.1订单id
    long orderId = redisIdWorker.nextId("order");
    voucherOrder.setId(orderId);
    //6.2用户id
    //Long userId = UserHolder.getUser().getId();
    voucherOrder.setUserId(userId);
    //6.3代金券id
    voucherOrder.setVoucherId(voucherId);

    //7.订单写入数据库
    save(voucherOrder);
    //8.返回订单Id
    return Result.ok(orderId);
}

(2) 导入依赖

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>

(3) 启动类上加 @EnableAspectJAutoProxy(exposeProxy = true) 暴露出代理对象

测试:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.8集群下的线程并发安全问题

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

Postman发送两个请求

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

两个id相同的进入到锁里面,证明没有被锁住,那就会生成2个订单

因为相当于我们开了两个jvm,所以有两个锁监视器,这样就出现并行的2个线程执行,这样就出现了线程安全问题

黑马Redis学习笔记 (基础篇+实战篇)

3.9分布式锁-原理

不去使用jvm内部的锁监视器,我们要在外部开一个锁监视器,让它监视所有的线程

黑马Redis学习笔记 (基础篇+实战篇)

多进程可见,互斥,高可用,高性能,安全性,. . . . .

常见的分布式锁

  • MySQL:MySQL 本身带有锁机制,但是由于 MySQL 性能一般,所以采用分布式锁的情况下,使用 MySQL 作为分布式锁比较少见。
  • Redis:Redis 作为分布式锁比较常见,利用 setnx 方法,如果 Key 插入成功,则表示获取到锁,插入失败则表示无法获取到锁。
  • Zookeeper:Zookeeper 也是企业级开发中比较好的一个实现分布式锁的方案。
MySQL Redis Zookeeper
互斥 利用 MySQL 本身的互斥锁机制 利用 setnx 互斥命令 利用节点的唯一性和有序性
高可用
高性能 一般 一般
安全性 断开链接,自动释放锁 利用锁超时时间,到期释放 临时节点,断开链接自动释放

3.10分布式锁-实现思路

黑马Redis学习笔记 (基础篇+实战篇)

问题: 如何做到添加锁操作和释放锁操作必须具备同成功同失败?

set操作和expire写在同个语句即可:set lock thread1 ex 10 nx (nx表示存在的时候才可以set)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.11实现Redis分布式锁版本1

黑马Redis学习笔记 (基础篇+实战篇)

ILock接口

public interface ILock {
    /**
     * 尝试获取锁
     * @Param timeoutSec 锁的持有时间
     * @return true:获取成功 false:获取失败
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
    
}

SimpleRedisLock

public class SimpleRedisLock implements ILock{
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private static final String KEY_PREFIX = "lock:";

    public SimpleRedisLock(String name,StringRedisTemplate stringRedisTemplate){
        this.name=name;
        this.stringRedisTemplate=stringRedisTemplate;
    }


    @Override
    public boolean tryLock(long timeoutSec) {
        String key = KEY_PREFIX + name;
        //value的话一般设置为哪个线程持有该锁即可
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId+"", timeoutSec, TimeUnit.SECONDS);
        //最好不要直接return success,因为我们返回的是boolean类型,现在得到的是Boolean的结果,就会进行自动装箱,如果success为null,就会出现空指针异常
        return Boolean.TRUE.equals(success);//null的话也是返回false
    }

    @Override
    public void unlock() {
        String key = KEY_PREFIX + name;
        stringRedisTemplate.delete(key);
    }
}

在VoucherOrderServiceImpl内使用:

Long userId = UserHolder.getUser().getId();
//创建锁对象
SimpleRedisLock lock = new SimpleRedisLock("order" + userId,stringRedisTemplate);
//获取锁
boolean hasLock = lock.tryLock(1200);
if(!hasLock){
    //获取锁失败: return fail 或者 retry 这里业务要求是返回失败
    return Result.fail("请勿重复下单!");
}

try {
    IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//获得代理对象
    return proxy.createVoucherOrder(voucherId);//默认是this,我们要实现事务需要proxy
} catch (IllegalStateException e) {
    throw new RuntimeException(e);
} finally {
    lock.unlock();
}

黑马Redis学习笔记 (基础篇+实战篇)

分布式锁的测试:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

证明现在只有一个线程获取锁成功了,8081线程持有了分布式锁,而8082没有,查看结果:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.12 Redis分布式锁误删问题

黑马Redis学习笔记 (基础篇+实战篇)

解决:在释放锁的时候判断锁的标识是否一致,Redis锁的标识一般是指value的区分,这里一般我们标识的是线程id,比如Thread1,Thread2…

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.13解决Redis分布式锁误删问题

黑马Redis学习笔记 (基础篇+实战篇)

这里的线程标识,我们之前用的是线程id进行标识,但是如果放到集群线程下,多个jvm可能会出现同个线程id的线程,这样会引发线程安全问题,所以这里要用ThreadID + UUID

@Override
public boolean tryLock(long timeoutSec) {
    String key = KEY_PREFIX + name;
    //value的话一般设置为哪个线程持有该锁即可
    //获取线程标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    //获取锁
    Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(key, threadId, timeoutSec, TimeUnit.SECONDS);
    //最好不要直接return success,因为我们返回的是boolean类型,现在得到的是Boolean的结果,就会进行自动装箱,如果success为null,就会出现空指针异常
    return Boolean.TRUE.equals(success);//null的话也是返回false
}

@Override
public void unlock() {
    String key = KEY_PREFIX + name;
    //获取线程标识
    String threadId = ID_PREFIX + Thread.currentThread().getId();
    //获取锁中的标识
    if (stringRedisTemplate.opsForValue().get(key).equals(threadId)) {
        stringRedisTemplate.delete(key);
    }
}

测试:线程1获取锁,然后我把锁的标识删除,让线程2获取锁

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

然后线程1就不会释放锁,线程2后面会释放锁

3.14 分布式锁的原子性问题

黑马Redis学习笔记 (基础篇+实战篇)

判断锁标识和释放锁是两个操作,这里有原子性问题

3.15 Lua脚本解决分布式锁的原子性问题

Lua语言调用Redis:

黑马Redis学习笔记 (基础篇+实战篇)

eg:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

-- 锁的key
-- local key = KEYS[1]
-- 当前线程标识
-- local threadId = ARGV[1]
-- 获取锁中的线程标识
local id = redis.call('get',KEYS[1])
-- 比较
if(id == ARGV[1]) then return redis.call('del',KEYS[1])
end return 0    

简化:

-- 比较
if(redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1])
end return 0

接下来就是java去调用lua,lua执行Redis

3.16java调用lua脚本改造分布式锁

黑马Redis学习笔记 (基础篇+实战篇)

unlock操作:

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;

static{//写成静态代码块,类加载就可以完成初始定义,就不用每次释放锁都去加载这个,性能提高咯
    UNLOCK_SCRIPT = new DefaultRedisScript<>();
    UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));//设置脚本位置
    UNLOCK_SCRIPT.setResultType(Long.class);
}
    public void unlock(){
        //调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId()
        );
    }

Lua脚本:

-- 比较
if(redis.call('get',KEYS[1]) == ARGV[1]) then return redis.call('del',KEYS[1])
end return 0

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

模拟超时释放锁: 8082拿到锁,把锁删除,8081也拿到锁,8082进行unlock的时候不会把8081的锁删除,8081unlock删除自己的锁

黑马Redis学习笔记 (基础篇+实战篇)

执行8082unlock后Redis中lock还在,8081执行unlock后就删除了

黑马Redis学习笔记 (基础篇+实战篇)

3.17分布式锁-Redission简介

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.18 Redisson快速入门

黑马Redis学习笔记 (基础篇+实战篇)

1.导入依赖

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

2.RedissonConfig,注入一个配置好的RedissonClient

@Configuration
public class RedissionConfig {
    @Bean
    public RedissonClient redissionClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.200.131:6379").setPassword("1234");
        //创建RedissonClient对象
        return Redisson.create(config);
    }
}

3.在我们的业务实现类中使用RedissionClient

@Autowired
private RedissonClient redissonClient;

业务方法{
//···前面流程
 Long userId = UserHolder.getUser().getId();
        //创建锁对象
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId,stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
        boolean hasLock = lock.tryLock( );
        if(!hasLock){
            //获取锁失败: return fail 或者 retry 这里业务要求是返回失败
            return Result.fail("请勿重复下单!");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();//获得代理对象
            return proxy.createVoucherOrder(voucherId);//默认是this,我们要实现事务需要proxy
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }
}

测试单线程:

黑马Redis学习笔记 (基础篇+实战篇)

测试多线程:记得登录验证token是否失效,失效了重新加

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.19 Redisson的可重入锁原理

为什么要引入可重入锁这种机制?

我们知道“对象一把锁,多个对象多把锁”,可重入锁的概念就是:自己可以获取自己的内部锁。

假如有一个线程 T 获得了对象 A 的锁,那么该线程 T 如果在未释放前再次请求该对象的锁时,如果没有可重入锁的机制,是不会获取到锁的,这样的话就会出现死锁的情况

这里除了记录key和ThreaId外,还需有记录重入次数,所以我们需要用hash, 每次的thead一样时,次数+1;

直到次数为0的时候才可以删除锁

nx :判断锁是否存在

ex: 设置过期时间

但是Redis的hash结构没有nx这个命令,所以我们只能先判是否存在(exist),再设置过期时间

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

@Slf4j
@SpringBootTest
public class RedissonTest {

    @Resource
    private RedissonClient redissonClient;

    private RLock lock;

    @BeforeEach
        // 创建 Lock 实例(可重入)
    void setUp() {
        lock = redissonClient.getLock("order");
    }

    @Test
    void methodOne() throws InterruptedException {
        boolean isLocked = lock.tryLock();
        log.info(lock.getName());
        if (!isLocked) {
            log.error("Fail To Get Lock~1");
            return;
        }
        try {
            log.info("Get Lock Successfully~1");
            methodTwo();
        } finally {
            log.info("Release Lock~1");
            lock.unlock();
        }
    }

    @Test
    void methodTwo() throws InterruptedException {
        boolean isLocked = lock.tryLock();
        if (!isLocked) {
            log.error("Fail To Get Lock!~2");
            return;
        }
        try {
            log.info("Get Lock Successfully!~2");
        } finally {
            log.info("Release Lock!~2");
            lock.unlock();
        }
    }
}

method1取到锁,redis内也存储lock:order:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.20 Redisson的锁重试和WatchDog机制

锁重试:获取锁失败后重新获取

tryLock(waitTime,leaseTime,TimeUnit)

waitTime:获取锁的等待时长,获取锁失败后等待waitTime再去获取锁

leaseTime: 锁自动失效时间,这里测试锁重试不需要用到

黑马Redis学习笔记 (基础篇+实战篇)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-89rNAz0V-1669222806273)(http://itsawaysu.oss-cn-shanghai.aliyuncs.com/note/Redisson%23tryLock%20%E9%94%81%E9%87%8D%E8%AF%95.png)]

上面不给leaseTime的话30s其实就是Lock WatchdogTimeOut,给到他,最后给到intrtnslLockleaseTime(用去watchdog续约)

tryLockInnerAsync其实就是执行lua脚本 state就是hashValue,就是次数

后面的ttl的结果只有nil或者30s,nil就是获取锁成功

WatchDog-----超时释放

对抢锁过程进行监听,抢锁完毕后,scheduleExpirationRenewal(threadId) 方法会被调用来对锁的过期时间进行续约,在后台开启一个线程,进行续约逻辑,也就是看门狗线程。

// 续约逻辑
commandExecutor.getConnectionManager().newTimeout(new TimerTask() {... }, 锁失效时间 / 3, TimeUnit.MILLISECONDS);

Method(new TimerTask(){}, 参数2, 参数3)

通过参数2、参数3 去描述,什么时候做参数1 的事情。

  • 锁的失效时间为 30s,10s 后这个 TimerTask 就会被触发,于是进行续约,将其续约为 30s;
  • 若操作成功,则递归调用自己,重新设置一个 TimerTask 并且在 10s 后触发;循环往复,不停的续约。

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

剩下的就是主从一致性问题

3.21 Redisson的multiiLock原理

黑马Redis学习笔记 (基础篇+实战篇)

三个结点:创建对应的三个client

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

//TODO

3.22 Redis优化秒杀

黑马Redis学习笔记 (基础篇+实战篇)

一人一单

key value1 value2 value3 … (value不重复) ----> set

黑马Redis学习笔记 (基础篇+实战篇)

3.23 基于Redis完成秒杀资格判断

完成需求1,2

黑马Redis学习笔记 (基础篇+实战篇)

1.新增优惠券的同时加入到Redis

黑马Redis学习笔记 (基础篇+实战篇)

测试:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

2.编写lua,基于lua完成一人一单

黑马Redis学习笔记 (基础篇+实战篇)

-- 1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2用户id
local userId = ARGV[2]

-- 2.数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:'..voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:'..voucherId

-- 3.脚本业务
-- 3.1判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0)
    then return 1
end
-- 3.2判断用户是否下单 sismember orderKey userId
if(redis.call('sismember',orderKey,userId)==1)
    then return 2
end
-- 3.3扣库存 incrby stockKey -1
redis.call('incrby',stockKey,-1)
-- 3.4下单 sadd orderKey userId
redis.call('sadd',orderKey,userId)
return 0

黑马Redis学习笔记 (基础篇+实战篇)

测试第一次领券

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

第二次领券:

黑马Redis学习笔记 (基础篇+实战篇)

3.24 基于阻塞队列实现秒杀异步下单

之前的代码逻辑:

黑马Redis学习笔记 (基础篇+实战篇)

完善后的完整代码:

 	private static final DefaultRedisScript<Long> SECKI_SCRIPT;

    static{//写成静态代码块,类加载就可以完成初始定义,就不用每次释放锁都去加载这个,性能提高咯
        SECKI_SCRIPT = new DefaultRedisScript<>();
        SECKI_SCRIPT.setLocation(new ClassPathResource("seckill.lua"));//设置脚本位置
        SECKI_SCRIPT.setResultType(Long.class);
    }

    private BlockingQueue<VoucherOrder> orderTasks = new ArrayBlockingQueue<>(1024*1024);//创建阻塞队列

    private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//创建线程池

// 判断库存和进行一人一单判断后将信息放入阻塞队列
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始,是否结束
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            return Result.fail("秒杀尚未开始!");
        }
        if(voucher.getEndTime().isBefore(LocalDateTime.now())){
            return Result.fail("秒杀已结束!");
        }
        //3.判断库存是否充足
        if(voucher.getStock()<=0){
            return Result.fail("优惠券库存不足!");
        }
    //获取当前用户
    Long userId = UserHolder.getUser().getId();
    //1.执行Lua脚本
    Long result = stringRedisTemplate.execute(
            SECKI_SCRIPT,
            Collections.emptyList(),//空List
            voucherId.toString(), userId.toString()
    );
    //2.判断结果是否0 是0就是成功,可下单,下单信息保存到阻塞队列
    if(result!=0){
        return Result.fail(result==1?"库存不足!":"不能重复下单!");
    }
    //生成订单id
    long orderId = redisIdWorker.nextId("order");
    //创建订单数据
    VoucherOrder voucherOrder = new VoucherOrder();
    voucherOrder.setUserId(userId);
    voucherOrder.setId(orderId);
    voucherOrder.setVoucherId(voucherId);
    //放入阻塞队列
    orderTasks.add(voucherOrder);
    //获取代理对象
    proxy = (IVoucherOrderService) AopContext.currentProxy();//获得代理对象
    //3.返回订单id
    return Result.ok(orderId);

}

// 类加载后就持续从阻塞队列出取出订单信息

@PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
    }

    private class VoucherOrderHandler implements Runnable{

        @Override
        public void run() {
            while(true){
                try {
                    //1.获取订单中的队列消息
                    VoucherOrder voucherOrder = orderTasks.take();
                    handleVoucherOrder(voucherOrder);
                    //2.创建订单
                } catch (Exception e) {
                    log.error("处理订单异常:",e);
                }
            }
        }
    }

//异步下单
private void handleVoucherOrder(VoucherOrder voucherOrder) {
        Long userId = voucherOrder.getUserId();//由于多线程,所以不能直接去ThreadLocal取
        //创建锁对象
        //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId,stringRedisTemplate);
        RLock lock = redissonClient.getLock("lock:order:" + userId);
        //获取锁
        boolean hasLock = lock.tryLock( );
        if(!hasLock){
            //获取锁失败
            log.error("不允许重复下单!");
            return;
        }

        try {
            //代理对象改成全局变量
            proxy.createVoucherOrder(voucherOrder);//默认是this,我们要实现事务需要proxy
        } catch (IllegalStateException e) {
            throw new RuntimeException(e);
        } finally {
            lock.unlock();
        }

    }

    @Transactional
    public void createVoucherOrder(VoucherOrder voucherOrder){
        //查询订单看看是否存在
        Long userId = UserHolder.getUser().getId();

        if (query().eq("user_id",userId).eq("voucher_id", voucherOrder.getUserId()).count()>0) {
            log.error("用户已经购买过一次!");
            return;
        }

        //4.扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock -1")
                .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0)//where id = ? and stock >0 添加了乐观锁
                .update();

        if(!success){
            log.error("优惠券库存不足!");
            return;
        }

        //7.订单写入数据库
        save(voucherOrder);
    }

3.25 认识消息队列

上一节我们基于jvm的阻塞队列进行秒杀存在2个问题:

  1. jvm的内存限制问题

  2. 数据安全问题:jvm的内存数据没有持久化,每当服务器重启或者宕机或者从阻塞队列取的时候遇到异常,数据都会丢失

    解决方法: 消息队列

黑马Redis学习笔记 (基础篇+实战篇)

Redis提供了三种不同方式来实现消息队列

1.list结构:模拟消息队列

2.Pubsub:基本的点对点模型

3.Stream :比较完善的消息队列模型

3.26 基于list实现的消息队列

黑马Redis学习笔记 (基础篇+实战篇)

xshell开两个一样的会话

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.27 基于PubSub实现的消息队列

黑马Redis学习笔记 (基础篇+实战篇)

xshell开三个一样的会话

两个消费者:

黑马Redis学习笔记 (基础篇+实战篇)

生产者发布消息:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

没人接收你的消息,消息就没了

3.28 基于Stream实现的消息队列

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)


黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

//获取最新消息
xread count 1 streams s1 $
//阻塞获取最新消息
xread count 1 block 0 streams s1 $ //block后面的数是阻塞毫秒数,0的话是永久阻塞

黑马Redis学习笔记 (基础篇+实战篇)

读最新数据会出现漏读现象:一下子发了5条最新消息,只读一条,其他4条漏读

黑马Redis学习笔记 (基础篇+实战篇)

3.29 Stream的消费者组模式

解决数据漏读的问题

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

不用自己去创建消费者,监听消息的时候然后发现无该消费者,则会自动创建

创建的时候ID注意如果想要之前的数据就从0开始,不想要就从$开始

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

3.30 基于stream消息队列实现异步秒杀下单

黑马Redis学习笔记 (基础篇+实战篇)

1.控制台stream类型创建消息队列

xgroup create stream.orders g1 0 mkstream

2.1 修改Lua脚本

-- 1.参数列表
-- 1.1 优惠券id
local voucherId = ARGV[1]
-- 1.2用户id
local userId = ARGV[2]
-- 1.3订单Id
local orderId = ARGV[3] -- 新增

-- 2.数据key
-- 2.1 库存key
local stockKey = 'seckill:stock:'..voucherId
-- 2.2 订单key
local orderKey = 'seckill:order:'..voucherId

-- 3.脚本业务
-- 3.1判断库存是否充足
if (tonumber(redis.call('get', stockKey)) <= 0)
    then return 1
end
-- 3.2判断用户是否下单 sismember orderKey userId
if(redis.call('sismember',orderKey,userId)==1)
    then return 2
end
-- 3.3扣库存 incrby stockKey -1
redis.call('incrby',stockKey,-1)
-- 3.4下单 sadd orderKey userId
redis.call('sadd',orderKey,userId)

--4.发送消息到消息队列, xadd stream.order * k1 v1 k2 v2
redis.call('xadd','stream.order','*','userId',userId,'voucherId',voucherId,'id',orderId) --新增
return 0

2.2 修改执行lua脚本

private IVoucherOrderService proxy;
public Result seckillVoucher(Long voucherId) {
    //1.查询优惠卷
    SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
    //2.判断秒杀是否开始,是否结束
    if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
        return Result.fail("秒杀尚未开始!");
    }
    if(voucher.getEndTime().isBefore(LocalDateTime.now())){
        return Result.fail("秒杀已结束!");
    }
    //3.判断库存是否充足
    if(voucher.getStock()<=0){
        return Result.fail("优惠券库存不足!");
    }
    //获取当前用户
    Long userId = UserHolder.getUser().getId();
    //生成订单id
    long orderId = redisIdWorker.nextId("order");
    //1.执行Lua脚本
    Long result = stringRedisTemplate.execute(
            SECKI_SCRIPT,
            Collections.emptyList(),//空List
            voucherId.toString(), userId.toString(),String.valueOf(orderId)
    );
    //2.判断结果是否0 是0就是成功,可下单,下单信息保存到阻塞队列
    if(result!=0){
        return Result.fail(result==1?"库存不足!":"不能重复下单!");
    }
    //获取代理对象
    proxy = (IVoucherOrderService) AopContext.currentProxy();//获得代理对象
    //3.返回订单id
    return Result.ok(orderId);

}

3.获取消息队列中的消息

private class VoucherOrderHandler implements Runnable{

    @Override
    public void run() {
        while(true){
            try {
                //1.获取消息队列中的消息 xreadgroup group g1 c1 count 1 block 2000 streams stream.orders >
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                        StreamOffset.create("stream.orders", ReadOffset.lastConsumed())
                );
                //2.判断获取是否成功
                //3.失败就再循环
                if(list==null||list.isEmpty()){
                    continue;
                }
                //4.成功就创建订单且ACK确认
                //解析消息
                MapRecord<String, Object, Object> record = list.get(0);//消息id,key,value
                //取出每个消息
                Map<Object, Object> values = record.getValue();
                //转为VoucherOrder实体类
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(values, new VoucherOrder(), true);
                //创建订单
                handleVoucherOrder(voucherOrder);
                //ACK确认
                stringRedisTemplate.opsForStream().acknowledge("stream.orders","g1",record.getId());
            } catch (Exception e) {
                log.error("处理订单异常:",e);
                //5.出现异常,从pendingList中取出数据后重新操作
                handlePendingList();
            }
        }
    }
}

private void handlePendingList() {
    while (true) {
        try {
            // 1. 获取 pending-list 中的订单信息
            // XREAD GROUP orderGroup consumerOne COUNT 1 STREAM stream.orders 0
            List<MapRecord<String, Object, Object>> readingList = stringRedisTemplate.opsForStream().read(
                    Consumer.from("g1", "c1"),
                    StreamReadOptions.empty().count(1),
                    StreamOffset.create("stream.orders", ReadOffset.from("0"))
            );

            // 2. 判断消息是否获取成功
            if (readingList.isEmpty() || readingList == null) {
                // 获取失败 pending-list 中没有异常消息,结束循环
                break;
            }

            // 3. 解析消息中的订单信息并下单
            MapRecord<String, Object, Object> record = readingList.get(0);
            Map<Object, Object> recordValue = record.getValue();
            VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(recordValue, new VoucherOrder(), true);
            handleVoucherOrder(voucherOrder);

            // 4. XACK
            stringRedisTemplate.opsForStream().acknowledge("stream.orders", "g1", record.getId());
        } catch (Exception e) {
            log.error("订单处理异常(pending-list)", e);
            try {
                // 稍微休眠一下再进行循环
                Thread.sleep(10);
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        }
    }
}

4.加锁,创建订单

private void handleVoucherOrder(VoucherOrder voucherOrder) {
    Long userId = voucherOrder.getUserId();//由于多线程,所以不能直接去ThreadLocal取
    //创建锁对象
    //SimpleRedisLock lock = new SimpleRedisLock("order:" + userId,stringRedisTemplate);
    RLock lock = redissonClient.getLock("lock:order:" + userId);
    //获取锁
    boolean hasLock = lock.tryLock( );
    if(!hasLock){
        //获取锁失败
        log.error("不允许重复下单!");
        return;
    }

    try {
        //代理对象改成全局变量
        proxy.createVoucherOrder(voucherOrder);//默认是this,我们要实现事务需要proxy
    } catch (IllegalStateException e) {
        throw new RuntimeException(e);
    } finally {
        lock.unlock();
    }

}

@Transactional
public void createVoucherOrder(VoucherOrder voucherOrder){
    //查询订单看看是否存在
    Long userId = UserHolder.getUser().getId();

    if (query().eq("user_id",userId).eq("voucher_id", voucherOrder.getUserId()).count()>0) {
        log.error("用户已经购买过一次!");
        return;
    }

    //4.扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock -1")
            .eq("voucher_id", voucherOrder.getVoucherId()).gt("stock",0)//where id = ? and stock >0 添加了乐观锁
            .update();

    if(!success){
        log.error("优惠券库存不足!");
        return;
    }

    //7.订单写入数据库
    save(voucherOrder);
}

四. 达人探店

4.1 发布探店笔记

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {

    @PostMapping("blog")
    public Result uploadImage(@RequestParam("file") MultipartFile image) {
        try {
            // 获取原始文件名称
            String originalFilename = image.getOriginalFilename();
            // 生成新文件名
            String fileName = createNewFileName(originalFilename);
            // 保存文件
            image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));
            // 返回结果
            log.debug("文件上传成功,{}", fileName);
            return Result.ok(fileName);
        } catch (IOException e) {
            throw new RuntimeException("文件上传失败", e);
        }
    }

    @GetMapping("/blog/delete")
    public Result deleteBlogImg(@RequestParam("name") String filename) {
        File file = new File(SystemConstants.IMAGE_UPLOAD_DIR, filename);
        if (file.isDirectory()) {
            return Result.fail("错误的文件名称");
        }
        FileUtil.del(file);
        return Result.ok();
    }

    private String createNewFileName(String originalFilename) {
        // 获取后缀
        String suffix = StrUtil.subAfter(originalFilename, ".", true);
        // 生成目录
        String name = UUID.randomUUID().toString();
        int hash = name.hashCode();
        int d1 = hash & 0xF;
        int d2 = (hash >> 4) & 0xF;
        // 判断目录是否存在
        File dir = new File(SystemConstants.IMAGE_UPLOAD_DIR, StrUtil.format("/blogs/{}/{}", d1, d2));
        if (!dir.exists()) {
            dir.mkdirs();
        }
        // 生成文件名
        return StrUtil.format("/blogs/{}/{}/{}.{}", d1, d2, name, suffix);
    }
}

4.2 查看探店笔记

@Override
public Result queryBlogById(Long id) {
    //1.查询blog
    Blog blog = getById(id);
    if(blog==null){
        return Result.fail("笔记不存在!");
    }
    //2.查询blog相关用户
    queryBlogUser(blog);
    return Result.ok(blog);
}

    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
@GetMapping("/{id}")
public Result queryBlogById(@PathVariable("id") Long id){
    return blogService.queryBlogById(id);
}

黑马Redis学习笔记 (基础篇+实战篇)

4.3 点赞功能

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

1.Blog类

/**
 * 是否点赞过了
 */
@TableField(exist = false)
private Boolean isLike;
@Override
public Result likeBlog(Long id) {
    //1.判断当前用户是否已点赞
    Long userId = UserHolder.getUser().getId();
    String key = BLOG_LIKED_KEY + id;
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    if(BooleanUtil.isFalse(isMember)){
        //2.未点赞:数据库赞+1
        boolean isSuccess = update().setSql("liked = liked +  1").eq("id", id).update();
        //3.用户信息保存到Redis的点赞set
        if(isSuccess){
            stringRedisTemplate.opsForSet().add(key,userId.toString());
        }
    }
   else{
        //4.已点赞:数据库-1
        boolean isSuccess = update().setSql("liked = liked -  1").eq("id", id).update();
        //5.把用户信息从Redis的点赞set移除
        if(isSuccess){
            stringRedisTemplate.opsForSet().remove(key,userId.toString());
        }
    }
   return Result.ok();
}
  1. 当我们点开一篇blog的时候就需要被看到是否点赞过,这就要求我们改一下queryBlogById(id)咯,当然isLikeBlog(blog)也是需要

    @Override
    public Result queryBlogById(Long id) {
        //1.查询blog
        Blog blog = getById(id);
        if(blog==null){
            return Result.fail("笔记不存在!");
        }
        //2.查询blog相关用户
        queryBlogUser(blog);
        //3.查询用户是否点过赞,其实就是给blog的isLike添加值
        isLikeBlog(blog);
    
        return Result.ok(blog);
    }
    
    private void isLikeBlog(Blog blog) {
        Long userId = UserHolder.getUser().getId();
        String key = BLOG_LIKED_KEY + id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        blog.setIsLike(BooleanUtil.isTrue(isMember));
    }
    
     @Override
        public Result queryHotBlog(Integer current) {
            // 根据用户查询
            Page<Blog> page = query()
                    .orderByDesc("liked")
                    .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
            // 获取当前页数据
            List<Blog> records = page.getRecords();
            // 查询用户
            records.forEach(blog->{
                this.queryBlogUser(blog);
                this.isLikeBlog(blog);
            });//就是用blog遍历的
            return Result.ok(records);
        }
    

测试:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.4 点赞排行榜

黑马Redis学习笔记 (基础篇+实战篇)

Redis的set存储的like无序,所以需要用到sortedset

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

id查和分页查blog的话内部也有查看是否点赞,这里的通用方法需要修改

黑马Redis学习笔记 (基础篇+实战篇)

进行top5的点赞用户查询:

@Override
public Result queryBlogLikes(Long id) {
    //1.查询top5的点赞用户   zrange key 0 4
    String key = BLOG_LIKED_KEY + id;
    Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);
    if(top5==null||top5.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    //2.解析出useId,然后根据UserId查询到user,再转化为UserDto
    List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    List<User> users = userService.listByIds(ids);
    List<UserDTO> userDTOS  =new ArrayList<>();
    for (User user : users) {
        UserDTO userDTO = new UserDTO();
        BeanUtils.copyProperties(user,userDTO);
        userDTOS.add(userDTO);
    }
    return Result.ok(userDTOS);
}
@GetMapping("/likes/{id}")
public Result queryBlogLikes(@PathVariable("id") Long id){
    return blogService.queryBlogLikes(id);
}

黑马Redis学习笔记 (基础篇+实战篇)

4.5关注和取关

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

curd:

@Override
public Result follow(Long followUserId, boolean isFollow) {
    //1.获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    if(isFollow){
        //关注
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        follow.setCreateTime(LocalDateTime.now());
        save(follow);
    }
    else{
        //取关
        QueryWrapper<Follow> queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_id",userId).eq("follow_user_id",followUserId);
        remove(queryWrapper);
    }
    return Result.ok();
}

@Override
public Result isfollow(Long followUserId) {
    //1.获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    //2.查询是否已关注 select count(*) from tb_follow where user_id = #{userId} and follow_user_id = #{followUserId};
    Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
    return Result.ok(count > 0);
}
@PutMapping("/{id}/{isFollow}")
public Result follow(@PathVariable("id") Long followUserId, @PathVariable("isFollow") boolean isFollow){
    return followService.follow(followUserId,isFollow);
}

@GetMapping("/or/not/{id}")
public Result isfollow(@PathVariable("id") Long followUserId){
    return followService.isfollow(followUserId);
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.6共同关注

首先是显示用户信息:

黑马Redis学习笔记 (基础篇+实战篇)

UserController

@GetMapping("/{id}")
public Result queryUserById(@PathVariable("id") Long userId){
    // 查询详情
    User user = userService.getById(userId);
    if (user == null) {
        return Result.ok();
    }
    UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
    // 返回
    return Result.ok(userDTO);
}

BlogController

@GetMapping("/of/user")
public Result queryBlogByUserId(
        @RequestParam(value = "current", defaultValue = "1") Integer current,
        @RequestParam("id") Long id) {
    // 根据用户查询
    Page<Blog> page = blogService.query()
            .eq("user_id", id).page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
    // 获取当前页数据
    List<Blog> records = page.getRecords();
    return Result.ok(records);
}

黑马Redis学习笔记 (基础篇+实战篇)

但是现在点击共同关注还是没有数据,这里是我们需补充的:求交集可以用Redis的set, 所以数据存放"备份"到Redis

黑马Redis学习笔记 (基础篇+实战篇)

修改之前的关注取关代码:

@Resource
private StringRedisTemplate stringRedisTemplate;

@Override
public Result follow(Long followUserId, boolean isFollow) {
    //1.获取当前登录用户
    Long userId = UserHolder.getUser().getId();
    String followKey = "follows:"+userId;
    if(isFollow){
        //关注
        Follow follow = new Follow();
        follow.setUserId(userId);
        follow.setFollowUserId(followUserId);
        follow.setCreateTime(LocalDateTime.now());
        boolean isSave = save(follow);
        if(isSave){
            //把被关注用户id放入Redis sadd follows:userId(key) followerId(value)
            stringRedisTemplate.opsForSet().add(followKey,followUserId.toString());
        }
    }
    else{
        //取关
        QueryWrapper<Follow> queryWrapper = new QueryWrapper();
        queryWrapper.eq("user_id",userId).eq("follow_user_id",followUserId);
         boolean isRemove = remove(queryWrapper);
            if(isRemove) {
                //把被关注用户id从Redis移除
                stringRedisTemplate.opsForSet().remove(followKey, followUserId.toString());
            }
    }
    return Result.ok();
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

接下来实现共同关注查询

@Override
public Result followCommons(Long followUserId) {
    //1.先获取当前用户
    Long userId = UserHolder.getUser().getId();
    String followKey1 = "follows:" + userId;
    String followKey2 = "follows:" + followUserId;

    //2.求交集
    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(followKey1, followKey2);
    if(intersect==null||intersect.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    //3.解析出id数组
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());

    //4.根据ids查询用户数组 List<User> ---> List<UserDTO>
    List<UserDTO> userDTOS = userService.listByIds(ids)
            .stream()
            .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
            .collect(Collectors.toList());

    return Result.ok(userDTOS);
}
@GetMapping("/common/{id}")
public Result followCommons(@PathVariable("id") Long followUserId){
    return followService.followCommons(followUserId);
}

测试:黑马Redis学习笔记 (基础篇+实战篇)

4.7 Feed流实现方案分析

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

用户关注的人一旦发布了新的笔记,就会第一时间推送给用户,所以我们需要选择TimeLine,我觉得还是消息队列好用

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

用户不多,所以选择推模式即可

4.8 推送到粉丝收件箱

黑马Redis学习笔记 (基础篇+实战篇)

不能使用传统的分页,因为每当有新数据进入的时候,就会出现角标变动,所以需要利用到滚动分页,记录每一次的lastId

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

发布后:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

查看1,1010用户的收件箱:

黑马Redis学习笔记 (基础篇+实战篇)

4.9 滚动分页查询收件箱

黑马Redis学习笔记 (基础篇+实战篇)

所以不能用角标 只能用score

黑马Redis学习笔记 (基础篇+实战篇)

limit后面的数据就是偏移量,决定取不取得到端点,第一次就要给0,之后的都要给1 (但是不行)

黑马Redis学习笔记 (基础篇+实战篇)

有相同值的话,用score的话会重复查

黑马Redis学习笔记 (基础篇+实战篇)

把limit后面的数字改为 上一次查询的最小值的重复数字的个数

4.10 实现滚动分页查询

黑马Redis学习笔记 (基础篇+实战篇)

public Result queryBloyOfFollow(Long max, Integer offset) {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2.查询当前用户收件箱 zrevrangebyscore key max min limit offset count
    String feedKey = FEED_KEY + userId;
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(feedKey, 0, max, offset, 2);
    if(typedTuples==null||typedTuples.isEmpty()){
        return Result.ok();
    }
    //3.解析出收件箱中的blogId,score(时间戳),offset
    List<Long> ids = new ArrayList<>(typedTuples.size());
    long minTime = 0;
    int count = 1;//最小时间的相同个数
    for (ZSetOperations.TypedTuple<String> typedTuple : typedTuples) {
        //3.1 获取id
        ids.add(Long.valueOf(typedTuple.getValue()));//blog的id
        //3.2 获取分数(时间戳)
        long time = typedTuple.getScore().longValue();
        if(time == minTime){
            count++;
        }else{
            minTime = time;
            count=1;
        }
    }
    //4.根据blogId查找blog
    String idStr = StrUtil.join(",",ids);
    List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id, " + idStr + ")").list();

    for (Blog blog : blogs) {
        //4.1 查询blog有关的用户
        queryBlogUser(blog);
        //4.2 查询blog是否被点过赞
        isLikeBlog(blog);
    }

    //5.封装并返回
    ScrollResult r = new ScrollResult();
    r.setList(blogs);
    r.setOffset(count);
    r.setMinTime(minTime);
    return Result.ok(r);
}
@GetMapping("/of/follow")
public Result queryBlogOfFollow(@RequestParam("lastId") Long max,@RequestParam(value = "offset",defaultValue = "0") Integer offset){
    return blogService.queryBloyOfFollow(max,offset);
}

测试:

黑马Redis学习笔记 (基础篇+实战篇)

再发送一条新笔记:

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.11 附近商铺-GEO数据结构的基本使用

附近商铺一般Es使用,这里的话还是用Redis

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

geoadd g1 116.37 39.86 bjn 116.42 39.90 bj 116.32 39.89 bjx 

黑马Redis学习笔记 (基础篇+实战篇)

geodist g1 bjx bj km(默认m)

黑马Redis学习笔记 (基础篇+实战篇)

geosearch g1  fromlonlat 116.39 39.90 byradius 10 km (asc|desc) (withdist)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.12附近商铺-导入店铺数据到GEO

黑马Redis学习笔记 (基础篇+实战篇)

geoadd的时候member的话存店铺id即可

黑马Redis学习笔记 (基础篇+实战篇)

数据导入Redis:

@Test
public void localshopData(){
    //1.查询店铺信息
    List<Shop> shops = shopService.list();
    //2.店铺按照typeId进行分组 map<typeId,店铺集合>
    //Map<Long,List<Shop>> map = shops.stream().collect(Collectors.groupingBy(shop -> shop.getTypeId()));
    Map<Long,List<Shop>> map = shops.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    //3.分批写入Redis
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
        //3.1获取类型id
        Long typeId = entry.getKey();
        String typeKey = SHOP_GEO_KEY + typeId;
        //3.2获取这个类型的所有店铺,组成集合
        List<Shop> value = entry.getValue();
        //3.3 写入Redis geoadd key 经度 维度 member
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>(value.size());
        for (Shop shop : value) {
            //stringRedisTemplate.opsForZSet().add(typeKey, new Point(shop.getX(),shop.getY()), shop.getId().toString());
            locations.add(new RedisGeoCommands.GeoLocation<>(
               shop.getId().toString(),
               new Point(shop.getX(),shop.getY())
            ));
        }
        stringRedisTemplate.opsForGeo().add(typeKey,locations);
    }
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.13附近商铺-实现附近商铺功能

黑马Redis学习笔记 (基础篇+实战篇)

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>org.springframework.data</groupId>
                    <artifactId>spring-data-redis</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>

        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-redis</artifactId>
            <version>2.6.2</version>
        </dependency>

        <dependency>
            <groupId>io.lettuce</groupId>
            <artifactId>lettuce-core</artifactId>
            <version>6.1.6.RELEASE</version>
        </dependency>

ShopServiceImpl

    public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断是否需要根据坐标进行查询
        if(x==null||y==null){
            Page<Shop> page = query().eq("type_id", typeId).page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            return Result.ok(page.getRecords());
        }
        //2.计算分页参数
        int from = (current-1)*SystemConstants.DEFAULT_PAGE_SIZE;
        int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
        //3.查询redis,按照距离排序,分页  geosearch bylonlat x y byredius 10 (km/m) withdistance
        String typeKey = SHOP_GEO_KEY + typeId;
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(
                        typeKey,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)//只能从0到end,后面需要自己截取
                );
        //4.解析出id
        if(results==null){
            return Result.ok(Collections.emptyList());
        }
        //4.1我们要的地方的list集合(店铺Id+distance)
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent();
        //有可能等下skip把数据都跳过了,所以需要判空
        if(content.size()<=from){
            //没有下一页
            return Result.ok(Collections.emptyList());
        }
        //4.2.截取first-end
        List<Long> ids = new ArrayList<>(content.size());
        Map<String,Distance> distMap = new HashMap<>(content.size());
        content.stream().skip(from).forEach(result ->{
            //4.2.1获取店铺Id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            //4.2.2获取距离
            Distance distance = result.getDistance();

            distMap.put(shopIdStr,distance);
        });//这里其实就是通过stream流将shopId提取出来,并且要根据距离进行排序

        //5.根据id查询shop
        String idStr = StrUtil.join(",",ids);//1,2,3,4...
        // .... where id in #{ids} order by field(id,1,2,3,4...) 根据id排序
        List<Shop> shops = query().in("id", ids).last("order by field(id," + idStr + ")").list();

        for (Shop shop : shops) {
            shop.setDistance(distMap.get(shop.getId().toString()).getValue());
        }
        return Result.ok(shops);
    }

ShopController

@GetMapping("/of/type")
public Result queryShopByType(
        @RequestParam("typeId") Integer typeId,
        @RequestParam(value = "current", defaultValue = "1") Integer current,
        @RequestParam(value = "x",required = false) Double x,
        @RequestParam(value = "y",required = false) Double y

) {
   return shopService.queryShopByType(typeId,current,x,y);
}

4.15用户签到-BitMap功能演示

不能直接用数据库表来存储数据

黑马Redis学习笔记 (基础篇+实战篇)

一看就是要010101,所以就是bit数组

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.16用户签到-实现签到功能

黑马Redis学习笔记 (基础篇+实战篇)

UserServiceImpl:

@Override
public Result sign() {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2.获取当前日期
    LocalDateTime now = LocalDateTime.now();
    //3.拼接key sign:1010:202211
    String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    //4.获取今天是本月的第几天
    int index = now.getDayOfMonth();//month是1~31,而offset是0~30
    //5.写入Redis setbit key offset 1
    stringRedisTemplate.opsForValue().setBit(key,index-1,true);//true即签到1
    return Result.ok();
}

UserController:

@GetMapping("/sign")//签到
public Result sign(){
    return userService.sign();
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.17 用户签到-统计连续签到

黑马Redis学习笔记 (基础篇+实战篇)

@Override
public Result signCount() {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2.获取当前日期
    LocalDateTime now = LocalDateTime.now();
    //3.拼接key sign:1010:202211
    String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    //4.获取今天是本月的第几天
    int index = now.getDayOfMonth();//month是1~31,而offset是0~30

    //1.获取本月截至今天为止的签到记录 bitfield sign:1010:202211 get u(index) 0 可以做多次操作,所以结果是list
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
            key,
            //子命令
            BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(index)).valueAt(0)
    );
    if(result==null||result.isEmpty()){
        //没有任何签到结果
        return Result.ok(0);
    }
    Long number = result.get(0);
    if(number==null||number==0){
        return Result.ok(0);
    }
    //2.number位运算求签到次数
    int count = 0;
    while(number>0){
        if((number&1)!=0){
            count++;
            number>>=1;
        }
        else break;
    }
    return Result.ok(count);
}
@GetMapping("/sign/count")
public Result signCount(){
    return userService.signCount();
}

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.18 UV统计-HyperLogLog的用法

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

黑马Redis学习笔记 (基础篇+实战篇)

4.19 UV统计-测试百万数据的统计

@Test
void testHyperLogLog(){
    String[] values = new String[1000];
    int j=0;
    for(int i=0;i<1000000;i++){
        j=i%1000;
        values[j]="user_"+i;
        if(j==999){
            stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
        }
    }
    //统计数量
    System.out.println(stringRedisTemplate.opsForHyperLogLog().size("hl2"));
}

测试一百万个数据,最后存入997593个数据,内存一开始是1679336,现在变成1693720,相差14384,也就是14.046875kb,没超过16kb

黑马Redis学习笔记 (基础篇+实战篇)

ap.put(shopIdStr,distance);
});//这里其实就是通过stream流将shopId提取出来,并且要根据距离进行排序

    //5.根据id查询shop
    String idStr = StrUtil.join(",",ids);//1,2,3,4...
    // .... where id in #{ids} order by field(id,1,2,3,4...) 根据id排序
    List<Shop> shops = query().in("id", ids).last("order by field(id," + idStr + ")").list();

    for (Shop shop : shops) {
        shop.setDistance(distMap.get(shop.getId().toString()).getValue());
    }
    return Result.ok(shops);
}

ShopController

```java
@GetMapping("/of/type")
public Result queryShopByType(
        @RequestParam("typeId") Integer typeId,
        @RequestParam(value = "current", defaultValue = "1") Integer current,
        @RequestParam(value = "x",required = false) Double x,
        @RequestParam(value = "y",required = false) Double y

) {
   return shopService.queryShopByType(typeId,current,x,y);
}

4.15用户签到-BitMap功能演示

不能直接用数据库表来存储数据

[外链图片转存中…(img-BVeaLslY-1669222806355)]

一看就是要010101,所以就是bit数组

[外链图片转存中…(img-VjL0sMpK-1669222806356)]

[外链图片转存中…(img-R1BWnuJQ-1669222806356)]

[外链图片转存中…(img-4soW8Ad1-1669222806356)]

[外链图片转存中…(img-AGP1n9wi-1669222806357)]

4.16用户签到-实现签到功能

[外链图片转存中…(img-23Wh4l3W-1669222806357)]

UserServiceImpl:

@Override
public Result sign() {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2.获取当前日期
    LocalDateTime now = LocalDateTime.now();
    //3.拼接key sign:1010:202211
    String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    //4.获取今天是本月的第几天
    int index = now.getDayOfMonth();//month是1~31,而offset是0~30
    //5.写入Redis setbit key offset 1
    stringRedisTemplate.opsForValue().setBit(key,index-1,true);//true即签到1
    return Result.ok();
}

UserController:

@GetMapping("/sign")//签到
public Result sign(){
    return userService.sign();
}

[外链图片转存中…(img-Ici0j30K-1669222806357)]

[外链图片转存中…(img-ESXMvZUH-1669222806357)]

4.17 用户签到-统计连续签到

[外链图片转存中…(img-mBMoWKPN-1669222806358)]

@Override
public Result signCount() {
    //1.获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2.获取当前日期
    LocalDateTime now = LocalDateTime.now();
    //3.拼接key sign:1010:202211
    String key = USER_SIGN_KEY + userId + now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
    //4.获取今天是本月的第几天
    int index = now.getDayOfMonth();//month是1~31,而offset是0~30

    //1.获取本月截至今天为止的签到记录 bitfield sign:1010:202211 get u(index) 0 可以做多次操作,所以结果是list
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
            key,
            //子命令
            BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(index)).valueAt(0)
    );
    if(result==null||result.isEmpty()){
        //没有任何签到结果
        return Result.ok(0);
    }
    Long number = result.get(0);
    if(number==null||number==0){
        return Result.ok(0);
    }
    //2.number位运算求签到次数
    int count = 0;
    while(number>0){
        if((number&1)!=0){
            count++;
            number>>=1;
        }
        else break;
    }
    return Result.ok(count);
}
@GetMapping("/sign/count")
public Result signCount(){
    return userService.signCount();
}

[外链图片转存中…(img-E5cKsdSE-1669222806358)]

[外链图片转存中…(img-p1r1Q2YP-1669222806358)]

4.18 UV统计-HyperLogLog的用法

[外链图片转存中…(img-78CvCdkL-1669222806358)]

[外链图片转存中…(img-eCqrGWtm-1669222806359)]

[外链图片转存中…(img-DstE1Vp6-1669222806359)]

4.19 UV统计-测试百万数据的统计

@Test
void testHyperLogLog(){
    String[] values = new String[1000];
    int j=0;
    for(int i=0;i<1000000;i++){
        j=i%1000;
        values[j]="user_"+i;
        if(j==999){
            stringRedisTemplate.opsForHyperLogLog().add("hl2",values);
        }
    }
    //统计数量
    System.out.println(stringRedisTemplate.opsForHyperLogLog().size("hl2"));
}

测试一百万个数据,最后存入997593个数据,内存一开始是1679336,现在变成1693720,相差14384,也就是14.046875kb,没超过16kb

[外链图片转存中…(img-vP23NmqT-1669222806359)]

黑马Redis学习笔记 (基础篇+实战篇)文章来源地址https://www.toymoban.com/news/detail-492579.html

到了这里,关于黑马Redis学习笔记 (基础篇+实战篇)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 黑马Redis视频教程实战篇(五)

    目录 一、达人探店 1.1、发布探店笔记 1.2、查看探店笔记 1.3、点赞功能 1.4、点赞排行榜 二、好友关注 2.1、关注和取消关注 2.2、共同关注 2.3、Feed流实现方案 2.4、推送到粉丝收件箱 2.4、实现分页查询收邮箱 发布探店笔记 探店笔记类似点评网站的评价,往往是图文结合。对

    2024年02月07日
    浏览(38)
  • 黑马Redis视频教程实战篇(一)

    目录 一、短信登录 1.1、导入黑马点评项目 (1)导入黑马点评sql脚本 (2)导入后端项目 (3)导入前端项目  1.2、基于Session实现登录流程 1.3 、实现发送短信验证码功能 1.4 、实现登录拦截功能  1.5 、隐藏用户敏感信息 (1)在登录方法处修改  (2)在拦截器处  (3)在

    2024年02月07日
    浏览(74)
  • 黑马Redis视频教程实战篇(二)

    目录 一、什么是缓存? 1.1 为什么要使用缓存? 1.2 如何使用缓存? 二、添加商户缓存 2.1 缓存模型和思路 2.2 代码实现 三、缓存更新策略 2.1 数据库缓存不一致解决方案 2.2 数据库和缓存不一致采用什么方案 四、实现商铺和缓存与数据库双写一致 五、缓存穿透问题的解决思

    2024年02月07日
    浏览(47)
  • 黑马点评Redis实战(优惠卷秒杀)

    本文是上一篇文章的后续,上一篇文章链接 马点评Redis实战(短信登录;商户查询缓存) id是一个订单必备的属性,而订单的id属性是必须唯一的,首先我们会想到使用数据库主键id,并设置为自增。这样似乎就能满足唯一性。 但是,这样会存在一些问题: id的规律太过明显,因

    2024年02月04日
    浏览(33)
  • 【go零基础】go-zero从零基础学习到实战教程 - 2项目初始化

    到项目初始化过程了,这边的项目设计完全按照作者自己的喜好来进行定义和设置的,所以各位完全可以按照自己的偏好自喜设置哈。 首先是创建一个工作文件夹哈。 别问为啥不直接quickstart,因为quickstart生成的api名字是greet,改起来很麻烦(头秃)。 注1: go-zero-demo是我随便

    2024年04月26日
    浏览(43)
  • 黑马版Redis基础篇

    关系型数据库:

    2024年02月08日
    浏览(26)
  • 《Redis 核心技术与实战》课程学习笔记(四)

    一旦服务器宕机,内存中的数据将全部丢失。目前,Redis 的持久化主要有两大机制,即 AOF 日志和 RDB 快照。 AOF 日志是如何实现的? 我们比较熟悉的是数据库的写前日志(Write Ahead Log, WAL),也就是说,在实际写数据前,先把修改的数据记到日志文件中,以便故障时进行恢复

    2024年02月12日
    浏览(45)
  • 《Redis 核心技术与实战》课程学习笔记(三)

    Redis 是单线程,主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的,这也是 Redis 对外提供键值存储服务的主要流程。但 Redis 的其他功能,比如持久化、异步删除、集群数据同步等,其实是由额外的线程执行的。 多线程的开销 我们刚开始增加线程数时,系统吞吐率

    2024年02月12日
    浏览(44)
  • 《Redis 核心技术与实战》课程学习笔记(六)

    在 Redis 主从集群中,哨兵机制是实现主从库自动切换的关键机制。 哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责的就是三个任务:监控、选主(选择主库)和通知。 监控 哨兵进程在运行时,周期性地给所有的主从库

    2024年02月13日
    浏览(42)
  • 《Redis 核心技术与实战》课程学习笔记(五)

    那我们总说的 Redis 具有高可靠性,又是什么意思呢? 其实,这里有两层含义:一是数据尽量少丢失,二是服务尽量少中断。 AOF 和 RDB 保证了前者,而对于后者,Redis 的做法就是增加副本冗余量,将⼀份数据同时保存在多个实例上。 即使有一个实例出现了故障,需要过一段时

    2024年02月13日
    浏览(54)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包