Redis基础二(spring整合redis)

Springboot整合Redis


一、Springboot整合redis

​ redis可以通过使用java代码来实现 第一部分文档中 在终端操作redis的所有命令,Spring已经帮我们封装了所有的操作,所以变得很简单了。

​ Spring专门提供了一个模块来进行这些操作的封装,这个模块就是Spring-data-redis,当然该模块下抽象出了很多接口,但是我们在开发中应用中你只要关注两个类就够了,分别是“org.springframework.data.redis.core.StringRedisTemplate”和 “org.springframework.data.redis.core.RedisTemplate”。

​ 这两个类封装了程序对 redis 的所有操作,而且当 SpringBoot 出来之后,在SpringBoot 中整合 Spring-data-redis 可以说是简单、优雅、大方、得体。


1.1 项目相关配置说明


  • 创建springboot工程,pom.xml中引入redis 相关依赖

    注意:注意版本冲突问题!

<dependencies><!--web启动器--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- redis --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- spring2.X集成redis所需common-pool2--><dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId></dependency>
</dependencies>

​ 在 spring-boot-starter-data-redis 依赖中就包含了另外两个依赖:

​ spring-data-redis 、lettuce-core

在这里插入图片描述


  • 配置文件 application.yml 中配置redis的相关信息
server:port: 9993spring:#应用名称application:name: spring_test_handler
# Redis配置redis:# Redis服务器地址host: 127.0.0.1# Redis服务器连接端口port: 6379# Redis服务器连接密码(默认为空)password: 123456..# 连接超时时间(毫秒)timeout: 1800000# Redis数据库索引(默认为0)database: 0# 连接池驱动的配置lettuce:pool:# 连接池中最大连接数max-active: 20# 取得连接的最大等待时间 -1 表示没限制max-wait: -1# 连接池中允许空闲连接的最大数量max-idle: 8# 连接池中空闲连接的最小数量min-idle: 2

1.2 使用 RedisTemplate

  • 创建测试类并执行代码:
package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;@SpringBootTest
public class RedisConnet {/***  自动注入 spring 提供好的操作 redis 的模板类 RedisTemplate* */@Autowiredprivate RedisTemplate redisTemplate;@Testpublic void testRedisTemplate(){//保存一个简单的数据存入redis中redisTemplate.opsForValue().set("name","张三");//从redis中获取存入的数据Object name = redisTemplate.opsForValue().get("name");System.out.println("从redis中获取的name数据为:"+name);}
}

​ 这里有个需要注意的点:

​ 在数据库中使用命令 get key 无法获取我们通过程序保存的” name : 张三 “值。 原因是我们使用 RedisTemplate 类操作的数据都是被转成了字节数组保存到 redis 中的,包括 Key 和 value 都被转换了

在这里插入图片描述

​ RedisTemplate 操作数据是将数据转化成字节数组保存到 redis,这是一个序列化的过程,取数据时将字节数组转换成对象是一个反序列化过程。 使用这种方式的优点是存取的速度快(因为数据被序列化了),缺点是存储的数据只能 java 程序读取(因为存储的时候是 java序列化)。

​ 如果只是要保存普通的字符串则使用 StringRedisTemplate

  • 也可以通过创建一个配置类,实现 redisTemplate 存入非二进制数据。

    配置类是固定的写法,直接引入即可。

package com.test.test_redis_j2cache.config;import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;/*** @ClassName : CachingConfigurerSupport* @Description : Redis配置类: 实现 RedisTemplate存入数据序列化方式的改变* @Author : AD*/
@EnableCaching
@Configuration
public class RedisConfig extends CachingConfigurerSupport {@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {RedisTemplate<String, Object> template = new RedisTemplate<>();RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);template.setConnectionFactory(factory);//key序列化方式template.setKeySerializer(redisSerializer);//value序列化template.setValueSerializer(jackson2JsonRedisSerializer);//value hashmap序列化template.setHashValueSerializer(jackson2JsonRedisSerializer);return template;}@Beanpublic CacheManager cacheManager(RedisConnectionFactory factory) {RedisSerializer<String> redisSerializer = new StringRedisSerializer();Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);//解决查询缓存转换异常的问题ObjectMapper om = new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);// 配置序列化(解决乱码的问题),过期时间600秒RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(600)).serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)).serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)).disableCachingNullValues();RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();return cacheManager;}
}

​ 同个配置类就可以实现 RedisTemplate 存入数据序列化的问题!


1.3 使用StringRedisTemplate

​ 当你的 redis 存的是字符串数据或者你要存取的数据就是字符串类型的时候,那么你就使用 StringRedisTemplate 就可以了。

​ 但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,此时可以将对象转换成 json 格式的数据再进行存储。

  • pom.xml中引入fastjson,对json数据格式的操作工具
<dependency><groupId>com.alibaba.fastjson2</groupId><artifactId>fastjson2</artifactId><version>2.0.43</version>
</dependency>
  • StringRedisTemplate测试类代码:
package com.test.test_redis_j2cache.testSpace;import com.alibaba.fastjson2.JSON;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;import java.util.HashMap;
import java.util.Map;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/
@SpringBootTest
public class RedisConnet {/***  自动注入 spring 提供好的操作 redis 的模板类 RedisTemplate* */@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testpublic void testStringRedisTemplate(){stringRedisTemplate.opsForValue().set("age","测试中文22");Object age = stringRedisTemplate.opsForValue().get("age");System.out.println("从redis中获取的age数据为:"+age);Map<String, Object> map = new HashMap<>();map.put("name","张三");map.put("age",22);map.put("sex","男");//通过fastjson 将map数据转换为json格式字符串在使用StringRedisTemplate进行存储stringRedisTemplate.opsForValue().set("map", JSON.toJSONString(map));}
}
  • 测试结果

在这里插入图片描述

​ 现在是面向json格式的字符串进行操作,优势是任何语言都可以读取存储的数据,因为基本上主流的语言都是支持json格式的数据。

总结

  • 其实两者之间的区别主要在于他们使用的序列化类:
    • RedisTemplate 使用的是 JdkSerializationRedisSerializer 存入数据会将数据先序列化成字节数组 后在存入Redis 数据库,取出数据的时候会将 redis 中的字节数组转换成对象(反序列化的过程)。
    • StringRedisTemplate 使用的是 StringReidsSerializer
  • RedisTemplate 是 StringRedisTemplate 的父类,在继承的过程中指定了泛型就是<String,String>

二、Springboot的redis注解介绍

​ 在开中某些方法查询的结果要放到缓存中(redis),下一次再调用该方法查询的时候先缓存中查找,如果缓存中有数据就直接返回,否则从关系型数据库中去查询数据。

​ 主配置文件中增加 : spring.cache.type = redis

​ 启动类/配置类 中增加: @EnableCaching

2.1 @Cacheable注解的使用

  • 封装Service层

      这里封装一个Service层,模拟一个查询数据库的方法,同时方法上使用  **@Cacheable** 注解。
    
package com.test.test_redis_j2cache.server;import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;import java.util.ArrayList;
import java.util.List;/*** @ClassName : CacheRedisServer* @Description : 测试@Cacheable注解,模拟server层查询数据库*/
@Service
public class CacheRedisServer {/*** Description: 缓存中有 names 就会直接返回(不在查询关系型数据库),没有则要查询* AOP 编程 --> 在执行该方法之前要先做一个查询缓存的增强处理 --> 既然是AOP操作,就需要先生成代理对象** @param* @return java.util.List<java.lang.String>*/@Cacheable(value = "names")public List<String> getList(){//模拟重数据库中查询数据List<String> list = new ArrayList<>();list.add("张三");list.add("王五");System.out.println("执行了查询数据库操作!!");return list;}
}
  • 创建一个测试类,模拟Controller层,调用Service层查询数据库获取数据
package com.test.test_redis_j2cache.testSpace;import com.test.test_redis_j2cache.server.CacheRedisServer;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/@SpringBootTest
public class RedisConnet {//注入查询数据库的Service层@Autowiredprivate CacheRedisServer cacheRedisServer;@Testpublic void testSpringCache() {System.out.println("====启动Test调用Server层方法=======");System.out.println("list = " + cacheRedisServer.getList());}
}

​ 通过执行Test方法两次,可以发现,Service层方法并未执行,而是AOP操作直接通过redis缓存中,获取到响应数据。

在这里插入图片描述


2.2 缓存 @Cacheable

​ 根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。查看源码,属性值如下:

属性/方法名解释
value缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames与value差不多,二选一即可
key可选属性,可以使用 SpEl 标签自定义缓存的key

  • 缓存 @CachePut

    ​ 使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。

    ​ 比如 addEmp 方法上可以添加注解(或者更新的方法上)。查看源码,属性值如下:

    属性/方法名解释
    value缓存名,必填,它指定了你的缓存存放在哪块命名空间
    cacheNames与value差不多,二选一即可
    key可选属性,可以使用 SpEl 标签自定义缓存的key

  • 缓存@CacheEvict

    ​ 使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上,查看源码,属性值如下:

    属性/方法名解释
    value缓存名,必填,它指定了你的缓存存放在哪块命名空间
    cacheNames与value差不多,二选一即可
    key可选属性,可以使用 SpEl 标签自定义缓存的key
    allEntries是否清空所有缓存,默认为false。如果指定为true,则方法调用后将立即清空所有的缓存

    注意: 缓存默认使用的是 RedisTemplate 的操作方式,使用注解的缓存策略实现保存数据的时候是将数据转换成字节数组去保存的,如果不需要字节数组,则需要额外的配置。改变缓存方式序列化策略。

配置类代码: [ 1.2 使用 RedisTemplate ] 节中的 RedisConfig ,redis配置类。


三、流水线操作Redis数据(Pipeline)

3.1 Pipeline概述

​ 在向 redis 操作数据的时候,比如保存数据就得向 redis 服务发送命令,要发送命令客户端和服务端必须建立连接,但是建立连接是需要耗费时间和性能的,在这种操作模式下,如果批量操作数据,比如要保存一万个对象,那么客户端和服务端就得建立一万次连接(虽然有了连接池,但是还是每次都需要从连接池中获取连接)。

在这里插入图片描述

​ 这样就会不断的创建连接浪费时间和性能,于是就出现了 redis 的流水线操作只需要取得一次连接,所有的命令都在一个连接中去发送,从而节省出了客户端和服务端频繁建立连接的时间提高了性能。

在这里插入图片描述

​ 将命令和结果打包之后一次发送,节省很多网络时间。Redis的命令是非常块的,微秒级的,网络却很慢,所以 pipeline 要做到的就是控制网络传输时间建立连接的时间 两种时间的开销!


3.2 Springboot 实现Pipeline

  • 传统方式实现redis保存一千个对象
package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;import java.util.HashMap;
import java.util.Map;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/
@SpringBootTest
public class RedisConnet {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 传统方式操作redis,保存1000个map数据 计算耗时* @param* @return void*/@Testpublic void testRedis1(){//记录开始时间long currentTimeMillis = System.currentTimeMillis();HashMap map = new HashMap();map.put("name", "张三");map.put("age", 22);map.put("sex", "男");for (int i = 0; i < 1000; i++) {redisTemplate.opsForValue().set("emp"+i,map);}//记录结束时间long newCurrentTimeMillis = System.currentTimeMillis();System.out.println("传统方式耗时:"+(newCurrentTimeMillis-currentTimeMillis));}
}

​ 通过传统方式可以大概记录出,向redis中存入1千条map数据总共消耗了34648ms:

在这里插入图片描述

  • 采用 流水线Pipeline 方式向redis存入1千条数据
package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/@SpringBootTest
public class RedisConnet {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 采用流水线方式 Pipeline 向redis存入1千条数据,计算耗时* @param* @return void*/@Testpublic void testRedis2() {//记录开始时间long currentTimeMillis = System.currentTimeMillis();HashMap map = new HashMap();map.put("name", "张三");map.put("age", 22);//返回值就是所有的执行结果保存到 list 集合中List list = redisTemplate.executePipelined(new RedisCallback<Object>() {@Overridepublic Object doInRedis(RedisConnection redisConnection) throws DataAccessException {for (int i = 0; i < 1000; i++) {redisConnection.set(("emp" + i).getBytes(), JSON.toJSONString(map).getBytes());}return null;}});//记录结束时间long newCurrentTimeMillis = System.currentTimeMillis();System.out.println("流水线方式耗时:" + (newCurrentTimeMillis - currentTimeMillis));System.out.println("执行pipeline命令的结果:"+list.toString());}
}

​ 通过测试,使用流水线 pipeline方式,仅仅只花费了 1615ms。

在这里插入图片描述

​ 在运行如此多的命令时,需要考虑的另一个问题是内存空间的消耗,因为对于程序而言,它最终会返回一个 List 对象,如果过多的命令执行返回的结果都保存到这个 List 中,一个对象如果占用内存过大可能会造成内存溢出,尤其在那些高并发的网站中就很容易造成 JVM 内存溢出的异常, 这个时候应该考虑使用迭代的方法执行 Redis命令。

  • 分批迭代执行 redis存储任务:
package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/@SpringBootTest
public class RedisConnet {/***  自动注入 spring 提供好的操作 redis 的模板类 RedisTemplate* */@Autowiredprivate RedisTemplate redisTemplate;/*** Description: pipeline 分批迭代方式向redis存入1千条数据* @return void* @date 2024-05-29*/public static int index;@Testpublic void testRedis3() {//记录开始时间long currentTimeMillis = System.currentTimeMillis();HashMap map = new HashMap();map.put("name", "张三");map.put("age", 22);//分批迭代方式向redis存入1千条数据for (int i = 0; i <10; i++) {//记录每次迭代的起始位置index =i*100;List list = redisTemplate.executePipelined(new RedisCallback() {@Overridepublic Object doInRedis(RedisConnection redisConnection) throws DataAccessException {for (int j = 0; j < 100; j++) {redisConnection.set(("emp" + (index + j)).getBytes(), JSON.toJSONString(map).getBytes());}return null;}});//返回值就是所有的执行结果保存到 list 集合System.out.println("第"+(i+1)+"次执行pipeline命令的结果:" + list.toString());}//记录结束时间long newCurrentTimeMillis = System.currentTimeMillis();System.out.println("分批迭代方式耗时:" + (newCurrentTimeMillis - currentTimeMillis));}
}

​ 如此操作,可以避免内存益处现象。

在这里插入图片描述

总结:

  1. 在使用流水线操作的时候添加数据一定要使用RedisConnection类对象。
  2. 流水线pipeline操作必须要把key 和 value 都转换成字节数组之后才能进行操作。

四、在Spring中实现Redis事务

​ 在 Redis 中事务默认也是自动提交的,如果要求事务手工提交则需要先开启事务,然后再
手工提交事务,提交了事务才会执行的命令。

4.1 在Spring中控制事务:

package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.*;import java.util.HashMap;
import java.util.List;
import java.util.Map;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/
@SpringBootTest
public class RedisConnet {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 测试redis 事务的使用, redisCallback 回调函数** @param* @return void* @date 2024-05-29*/@Testpublic void testRedisCallback(){//创建事务回调函数,并封装命令SessionCallback sessionCallback = new SessionCallback(){@Overridepublic Object execute(RedisOperations redisOperations) throws DataAccessException {//开启事务redisOperations.multi();Object result = null;try {//执行命令redisOperations.opsForValue().set("name","张三");redisOperations.opsForValue().set("age",22);redisOperations.opsForValue().increment("age"); //年龄自增长加1//提交事务result = redisOperations.exec();}catch (Exception e){//事务回滚redisOperations.discard();e.printStackTrace();System.out.println("sessionCallback 事务回滚:"+e.getMessage());}return result;}};//执行事务Object execute = redisTemplate.execute(sessionCallback);System.out.println("事务执行结果:"+execute.toString());}
}

​ 事务操作执行结果:

在这里插入图片描述

总结: 在 redis 中,对于一个存在问题的命令,如果在入队的时候就已经出错,之前的入队的命令可以被回滚(回滚的本质是清空队列中的命令),如果这个错误命令在入队的时候并没有报错,而是在执行的时候出错了 ,那么 redis 默认跳过这个命令执行后续命令(无法回滚其他正确的命令)。也就是说 redis 事务没有完全实现事务的原子性,所以 redis 的事务又叫做弱事务。


4.2 Redis实现的乐观锁

Redis 自己提供了乐观锁的支持,基本步骤如下:

  1. A用户对数据开启监视: 在这里会读取出要监视的数据的值
  2. 开启事务
  3. 此时B用户修改了被 A 用户监视的数
  4. A 用户再次修改数据的时候是需要使用 exec 命令提交事务的,但是此时会在提交之前发现了监控的数据不一样了(监控到数据发生了改变),则当前的事务提交失败了。 在 Redis 中是直接支持乐观锁的。
  • Springboot实现乐观锁
package com.test.test_redis_j2cache.testSpace;import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.core.*;import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** @ClassName : RedisConnet* @Description : redis连接测试* @Author : AD*/@SpringBootTest
public class RedisConnet {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 测试Redis 乐观锁的实现** @param* @return void* @date 2024-05-29*/@Testpublic void testRedisLock(){//创建事务回调函数,并封装命令SessionCallback sessionCallback = new SessionCallback(){@Overridepublic Object execute(RedisOperations operations) throws DataAccessException {operations.opsForValue().set("age",18);//开启监视数据(乐观锁的实现)operations.watch("age");//开启事务operations.multi();try {System.out.println("线程进入睡眠状态 10s");TimeUnit.SECONDS.sleep(10);}catch (Exception e) {e.printStackTrace();}//事务中修改监视数据ageoperations.opsForValue().set("age",100);return operations.exec();}};Object execute = redisTemplate.execute(sessionCallback);System.out.println("事务执行结果打印:"+execute.toString());Object age = redisTemplate.opsForValue().get("age");System.out.println("目前redis中age的值:"+age);}
}

​ 在没有B用户的干预情况下,java测试类正常执行后的输出结果为:

在这里插入图片描述

  • 现在重新执行一次测试类方法,并在线程睡眠的 10s 时间内,(即A用户java测试类提交了一个key=age 的数据,并且对该数据进行了监视),在对 key = age 数据进行修改操作(模拟用户B),之后看测试类的运行结果。

    在这里插入图片描述

    ​ 查看测试类这边的输出结果:

    在这里插入图片描述

    可以发现,被监视的数据,中途被B用户修改之后,A用户(测试类)对age参数的修改是没有成功的。最后的age数据也变为了 B用户修改的200。这就是乐观锁的实现。


五、分布式锁

​ 在传统单体应用单机部署的情况下,为了保证一个方法或属性在高并发情况下的同一时间只能被同一个线程执行,可以使用并发处理相关的功能进行互斥控制。

​ 随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上(跨 JVM 进程),这将使原单机部署情况下的并发控制锁策略失效,原始的互斥控制就不能提供分布式锁的能力。为了解决这个问题就需要一种跨机器的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题!

分布式锁主流的实现方案:

  1. 基于数据库实现分布式锁
  2. 基于缓存(Redis等)
  3. 基于Zookeeper

每一种分布式锁解决方案都有各种的优缺点,性能最高的是redis实现,可靠性最强的是zookeeper。


5.1 分布式锁场景

​ 在分布式开发的系统之中,一定会在项目之中出现有若干个业务中心,那么某些操作就需要通过总的业务中心协调这些所有子业务中心的调用,但是,所有的数据库都在各自的业务中心上,那么在这样的情况下,总业务层是无法使用传统的方式进行互斥控制的,我们只能通过分布式锁来解决。

​ 这里采用秒杀业务作为场景,来实现分布式锁的构建。

在这里插入图片描述

​ 上图中的秒杀业务场景中可能会出现一个问题:因为有很多用户同时参与秒杀活动。如果商品还剩下1件时,A用户秒杀成功后库存减为0,但是还没提交事务;此时 B 用户查询到库存还是1,继续参与秒杀活动并且成功。这样就会出现超卖现象。

​ 在之前单服务器部署项目的时候我们可以使用传统的互斥锁解决这一问题,但是现在是分布式项目,所以之前传统的锁无法实现我们的需求了,于是分布式锁就登场了。

​ 要解决这个问题就得使用分布式锁的思想来实现了,分布式锁的实现思路: 第一个用户先到则马上去到 redis 中去注册锁,注册成功则认为获取了锁,取得当前秒杀的操作权,当库存管理和订单处理业务都执行完毕之后就
去 redis 中去注销(释放锁),后面来访问的用户需要到 redis 中查看业务是否被其他用户注册了,如果被注册了就等待释放锁。

在这里插入图片描述


5.2 redis中实现分布式锁演示

​ 分布式锁主要是通过 setnx 命令来实现。

# 设置锁,setnx命令会判断该key是否存在,不存在返回1并且设置value值,存在返回0
setnx user 10 
# 释放锁,通过del 删除锁
del user
# 释放锁成功之后下一个用户才可 通过 setnx 来获取锁

​ 那这种实现方式有个问题存在:现在上锁之后,锁都是我们程序员手动释放的。如果我们因为一些原因没有释放锁,那锁就会一直存在。

​ 解决该问题的方案很简单,就是上锁以后,给锁设置一个过期时间,当时间到达之后,锁就自动释放了。

setnx user 10  # 设置锁
expire user 20  # 设置锁的过期时间 20秒

​ 现在我们设置锁,并且设置锁的失效时间是由两个命令实现的。并不具备原子性。如果上锁成功,还没来得及设置锁的过期时间,redis服务就挂掉了,那锁的失效时间还是没有设置成功,如何解决?

​ 我们可以在上锁的同时设置过期时间。

setnex user 10 nx ex 20 # 上锁的同时 设置过期时间是20
ttl user # 查看指定key值的存活时期 不存在就为负数

5.3 springboot 实现分布式锁

5.3.1 分布式锁核心实现代码
  • 定义分布式锁工具类(核心程序)
package com.test.test_redis_j2cache.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @ClassName : RedisLockUtil* @Description : 分布式锁实现工具类* @Author : AD*/
@Component
public class RedisLockUtil {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 获取锁:本质就是在 redis 中保存一个键值** @param key 在 redis 中注册锁的 key 值* @param uid 用户的编号,作为锁的 value* @param time 超时时间 超过了指定的时间则锁要自动释* @param timeUnit 时间单位* @return java.lang.Boolean*/public Boolean lock(String key, String uid, Long time, TimeUnit timeUnit){//调用命令 setnxBoolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, uid, time, timeUnit);return aBoolean;}/*** Description: 释放锁的方法:本质就是删除 redis 中的锁的 ke** @param key 要删除的数据的 key* @param uid* @return java.lang.Boolean*/public Boolean unlock(String key, String uid){//先查询判断是否存在锁String lock =(String) redisTemplate.opsForValue().get(key);//可以释放锁if (lock != null){//判断锁是否是自己的锁,防止释放到其他人的锁if (lock.equalsIgnoreCase(uid)){return redisTemplate.delete(key);}}return false;}}

​ RedisLockUtil 工具类,主要封装了分布式锁的获取和释放方法。在业务处理之前先通过调用工具类中的获取锁方法,获取锁成功之后在执行后续操作,最后需要释放锁。


  • 迷模拟秒杀活动的控制层Controller封装:
package com.test.test_redis_j2cache.controller;import com.test.test_redis_j2cache.util.RedisLockUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.ServletRequest;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;/*** @ClassName : TestRedisLockController* @Description : 测试Redis分布式锁测试Controller* @Author : AD*/
@RestController
public class TestRedisLockController {@Autowiredprivate RedisLockUtil redisLockUtil;/*** 定义分布式锁Key* */private String lockKey = "redisLock";/*** 指定库存数量* */private static int STOCK = 3;/*** Description: 模拟秒杀活动下单访问接口。*/@GetMapping("/testSpike")public String testSpikeMethod(ServletRequest request){//以当前请求线程的名称作为用户的编号//String uid = Thread.currentThread().getName();//以当前请求线程的session id作为用户的编号HttpServletRequest httpServletRequest =(HttpServletRequest) request;String uid = httpServletRequest.getSession().getId().substring(0,5);System.out.println("======== 当前参与秒杀用户"+uid+" ========");try {while (true){//尝试获取锁if (redisLockUtil.lock(lockKey, uid,30L, TimeUnit.SECONDS)){//判断库存是否充足if (STOCK > 0){//秒杀任务抢锁成功,同时库存还有剩余,扣减库存System.out.println("uid="+uid+"-->[获取到锁,正在执行秒杀业务....");TimeUnit.SECONDS.sleep(4);//扣减库存--STOCK;System.out.println("uid="+uid+"-->[秒杀成功,库存剩余:"+STOCK+"]");return "亲爱的 " + uid + "用户你好,你已经成功参与秒杀,请稍后查看!";}System.out.println("uid="+uid+"-->[秒杀失败,库存余额不足!!!!!!!");return "uid="+uid+"-->秒杀失败,该商品被抢空了,请选择其他商品!";}else {//获取锁失败,说明该用户已经抢占了锁,等待30秒后重试System.out.println("uid="+uid+"-->没有获取到锁,重新尝试获取锁....");TimeUnit.MILLISECONDS.sleep(2000);}}}catch (Exception e){e.printStackTrace();return "服务器繁忙异常,请稍后重试...";}finally {//释放锁操作!Boolean unlock = redisLockUtil.unlock(lockKey, uid);String sout = unlock ? "释放锁操作成功!" : "释放锁操作失败!";System.out.println("uid="+uid+"-->"+sout);}}
}

​ 封装好Controller层后,我们同时访问多次该接口,来模拟高并发情况。

访问结果如下:

在这里插入图片描述


5.3.2 解决分布式锁续期问题

​ 如果出现了业务时间很长,那么可以续期,也就是要将当前用户的锁延期再释放。使用一个线程在用户取得锁之后的开始计算时间,当到超时时间的三分之二的时候去 redis中查看当前用户是否还占有锁,如果占有锁意味着可能当前业务可能要超时,此时就为当前用户的锁续期,续期可以续为超时的三分之二,本次案例续期一次(具体根据需求)

  • 修改RedisLockUtil工具类,添加分布式锁续期的方法。
package com.test.test_redis_j2cache.util;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;import java.util.concurrent.TimeUnit;/*** @ClassName : RedisLockUtil* @Description : 分布式锁实现工具类* @Author : AD*/
@Component
public class RedisLockUtil {@Autowiredprivate RedisTemplate redisTemplate;/*** Description: 获取锁:本质就是在 redis 中保存一个键值** @param key 在 redis 中注册锁的 key 值* @param uid 用户的编号,作为锁的 value* @param time 超时时间 超过了指定的时间则锁要自动释* @param timeUnit 时间单位* @return java.lang.Boolean*/public Boolean lock(String key, String uid, Long time, TimeUnit timeUnit){//调用命令 setnxBoolean aBoolean = redisTemplate.opsForValue().setIfAbsent(key, uid, time, timeUnit);if (aBoolean){//如果获取分布式锁成功,同时需要执行为当前业务续期的问题(也就是要给当前业务获取到的锁加时间)extendLockTime(key,uid,time,timeUnit);}return aBoolean;}/*** Description: 释放锁的方法:本质就是删除 redis 中的锁的 ke** @param key 要删除的数据的 key* @param uid* @return java.lang.Boolean*/public Boolean unlock(String key, String uid){//先查询判断是否存在锁String lock =(String) redisTemplate.opsForValue().get(key);//可以释放锁if (lock != null){//判断锁是否是自己的锁,防止释放到其他人的锁if (lock.equalsIgnoreCase(uid)){return redisTemplate.delete(key);}}return false;}/*** Description: 续期锁的方法:本质就是更新 redis 中的锁的超时时间。这里只做了一次续期操作。** @param key 分布式锁Key* @param uid 用户的编号,作为锁的 value* @param extendTime 分布式锁续期时间* @param timeUnit 时间单位* @return java.lang.Boolean*/public void extendLockTime(String key,String uid,Long extendTime,TimeUnit timeUnit){//从成功获取锁开始计算 往后推 超时时间的三分之二 去检测当前用户是否还持有锁,如果持有锁,表示该业务可能超时(原始的超时时间不够处理业务),则需要为其续期//能否在此直接休眠? 如果在这里休眠 则主线程都休眠了,所以不能在此直接休眠//创建一个子线程来实现续期Thread thread = new Thread(() -> {try {//休眠时间为 超时时间的三分之二TimeUnit.SECONDS.sleep(extendTime/3*2);//去检测当前业务是否还持有锁if (uid.equalsIgnoreCase((String) redisTemplate.opsForValue().get(key))){//为当前用户进行锁续期redisTemplate.expire(key, extendTime, timeUnit);System.out.println("当前用户:"+uid+" ---> 分布式锁续期成功!");}} catch (InterruptedException e) {throw new RuntimeException(e);}});//线程要设置为守护线程thread.setDaemon(true);//将线程启动thread.start();}
}

​ 这里还需要将修改Contrller层封装的秒杀接口,将锁自动释放时间设置为3秒,模拟业务执行需要时间设置为4s,然后调用秒杀接口查看输出日志:

在这里插入图片描述

​ 上面的锁续期只能续期一次,有可能出现续期之后时间还是不够,可以根据需求设置续期具体次数(比如 3 次),超过 3 次则认为当前用户操作的业务出现问题了,强制释放锁,但是要记录好日志,同时发送消息给运维人员人工处理,一般续期时是过期时间的三分之二。


5.3.3 秒杀业务存在的问题记录
  • 当请求获取到锁–>正在执行业务逻辑,当前还没有释放锁,但是这个时候程序挂掉了,那么该请求将永远拿到该锁不会再去释放,别的请求将无法获取到锁。

    ​ 解决方案:在获取锁的时候,为锁同时添加一个过期时间

  • 释放别人的锁 --> A用户已经获取到锁,在执行业务,但是还没有执行完成,过期时间到了(这里不考虑续期),那么该锁就会被自动释放[key 值自动失效](但是A用户后面还会执行 unlock释放锁业务);此时请求 B 能够获取该锁,且执行业务逻辑。此时请求 A 执行完成需要释放锁,但是此时释放的锁是请求 B 的,也就是释放别人的锁。

    ​ 释放别人的锁解决方法:在上述获取锁的时候,我们仅单单设置了 key 值,但是 value 设置的为 null,我们可以将上述版本优化为在拿取锁的时候同时设置 key 和value,将 value 设置为随机数,在释放锁的时候,先去判断一下该 key 的 value 值是否是之前设置的 value 值,是的话说明是自己的锁,进行释放,否则不是。

    通过 K V 同时判断是否是自己的分布式锁,这样就避免了释放别人的锁的问题。

  • 业务没有执行完毕,但是分布式锁时间已经过期,这种问题可以采用写一个守护线程,然后每隔固定时间去查看 redis 锁是否过期,如果没有过期的话就延长其过期时间,也就是为其锁续期

    ​ 上面也就是俗称的看门狗机制,上述逻辑已经有技术实现——Redission


    为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件

  • 互斥性。在任意时刻,只有一个客户端能持有锁。

  • 不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。

  • 解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。

  • 加锁和解锁必须具有原子性。


六、Redisson实现分布式锁

​ 实现Redis的分布式锁,除了自己基于redis client 原生api 来实现之外,还可以使用开源框架:Redisson。

​ Redisson是一个企业级的开源 Redis Client,也提供了分布式锁的支持, Redisson就是用于在 Java 程序中操作 Redis 数据库,它使得我们可以在程序中轻松地使用 Redis。Redisson 在java.util 中常用接口的基础上,为我们提供了一系列具有分布式特性的工具类。
Redisson底层采用的是 Netty 框架。支持 Redis 2.8 以上版本,支持 Java1.6+以上版本。

  • 添加Redisson 依赖

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId>
</dependency><!--web启动器-->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- redis -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring2.X集成redis所需common-pool2-->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency><!-- Redisson:提供了一系列具有分布式特性的工具类,包括分布式锁,分布式集合,分布式消息等。 -->
<dependency><groupId>org.redisson</groupId><artifactId>redisson</artifactId><version>3.14.1</version>
</dependency

​ 在项目的 pom.xml 依赖管理文件中引入 redisson开源框架的依赖 org.redisson.Redisson。


  • 配置 Redisson 定义配置类
package com.test.test_redis_j2cache.config;import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;/*** @ClassName : RedissonConfig* @Description : 开源框架分布式锁Redisson实现  初始化配置类* @Author : AD*/@Component
public class RedissonConfig {@Value("${spring.redis.host}")private String HOST ;@Value("${spring.redis.port}")private String PORT ;@Value("${spring.redis.password}")private String PASSWORD ;/*** Description: 单机模式初始化RedissonClient** @param* @return org.redisson.api.RedissonClient*/@Beanpublic RedissonClient redisClient() {Config config = new Config();config.useSingleServer().setAddress("redis://"+HOST+":"+PORT).setPassword(PASSWORD);return Redisson.create(config);}
}
  • 通过 Redisson 框架,完成分布式锁的实现:
package com.test.test_redis_j2cache.controller;import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;/*** @ClassName : RedissonLockController* @Description : Redisson框架实现分布式锁功能!!* @Author : AD*/@RestController
public class RedissonLockController {@AutowiredHttpServletRequest request;@Autowiredprivate RedissonClient redissonClient;/*** 规定好分布式锁的key值* */private static final String LOCK_KEY ="RedissonLock";/*** 定义好库存数量* */private static int STOCK = 3;@GetMapping("testRedisson")public String redissonLockTest(){//以requestId作为 用户id,区别请求唯一性String uid = request.getSession().getId().substring(0, 5);//公平锁 保证 Redisson 客户端线程将以其请求的顺序获得锁//获取锁 这里使用 while 循环 实现了一个自旋锁的RLock fairLock = redissonClient.getFairLock(LOCK_KEY);try {// 具有 Watch Dog 自动延期机制 默认续 30s 每隔 30/3=10fairLock.lock();System.out.println("【" + uid + "】 获取到了锁,正在执行秒杀的业务.....");if (STOCK > 0){//表示获取到锁了//正常是需要调用其他子业务中心的(比如说库存管理和订单管理中心TimeUnit.SECONDS.sleep(2);//减去库存--STOCK;System.out.println("【" + uid + "】 秒杀成功,剩余库存:" + STOCK);return "秒杀成功,剩余库存:" + STOCK;}else {System.out.println("【" + uid + "】 秒杀失败,库存已经被抢光!!");return "秒杀失败,库存已经被抢光!!";}}catch (Exception e){e.printStackTrace();return "服务器繁忙,请稍后重试!";}finally {//释放锁System.out.println("【" + uid + "】 准备释放锁!");if (fairLock.isLocked() && fairLock.isHeldByCurrentThread()){fairLock.unlock();}}}
}

​ 多次调用该请求接口测试结果如下:

在这里插入图片描述


总结:

  1. 关于分布式锁的实现方式有很多种,并不只是可以使用 redis 实现,只不过使用redis 是更为简便和性能更高的选择,所以核心是你要理解分布式锁的思想,具体使用什么技术实现其实可以根据实际情况自己选择的,比如可以使用 mysql 实现,也可以使用 zookeeper 实现。
  2. 每一种方案都有其自己的优势和劣势,包括 redis 也有自己的不足,但是总体来说瑕不掩瑜,关于分布式锁中还有很多细节没有考虑,比如高可用层面的问题(当前的 redis 如果宕机了则会导致整个系统瘫痪)等。
  3. 就算是使用同一种技术来实现分布式锁,也都有不同的实现方案,比如案例还可以使用:面向切面的编程,使用 SpringAop 的前置通知(取得锁)和后置通知(释放锁)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/55800.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Linux】详解Linux下的工具(内含yum指令和vim指令)

文章目录 前言1. Linux下软件安装的方式2. yum2.1 软件下载的小知识2.2 在自己的Linux系统下验证yum源的存在2.3 利用yum指令下载软件2.4 拓展yum源&#xff08;针对于虚拟机用户&#xff09; 3. vim编辑器3.1 vim是什么&#xff1f;3.2 如何打开vim3.2 vim各模式下的讲解3.2.1…

Oracle中ADD_MONTHS()函数详解

文章目录 前言一、ADD_MONTHS()的语法二、主要用途三、测试用例总结 前言 在Oracle数据库中&#xff0c;ADD_MONTHS()函数用于在日期中添加指定的月数。 一、ADD_MONTHS()的语法 ADD_MONTHS(date, n) 其中&#xff0c;date是一个日期值&#xff0c;n是一个整数值&#xff0c…

基于vue框架的大学生学业预警系统设计与实现53ify(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。

系统程序文件列表 项目功能&#xff1a;学生,公告信息,成绩信息,科目,学分信息,考勤信息,教师 开题报告内容 基于Vue框架的大学生学业预警系统设计与实现开题报告 一、研究背景与意义 随着高等教育的普及与深入&#xff0c;大学生群体规模日益扩大&#xff0c;其学业管理成…

百元头戴式耳机哪款口碑爆棚+质价比高?2024耳机最强推荐攻略!

在2024年的耳机市场中&#xff0c;百元头戴式耳机凭借其亲民的价格和出色的性能&#xff0c;成为了众多消费者的首选。随着技术的不断进步&#xff0c;这一价位段的耳机不仅在音质上有了显著提升&#xff0c;还在舒适度、降噪能力以及续航时间等方面表现出色。那百元头戴式耳机…

CAN XL协议标准在CANoe中的应用

众所周知&#xff0c;CAN通信技术在汽车领域中&#xff0c;有着非常广泛的应用。从1991年&#xff0c;第一代经典CAN在奔驰S级轿车中首次应用&#xff1b;到2011年&#xff0c;开始第二代CAN总线&#xff08;即CAN FD&#xff09;的开发&#xff1b;如今&#xff0c;ISO 11898-…

MyBatis 操作数据库入门

目录 前言 1.创建springboot⼯程 2.数据准备 3.配置Mybatis数据库连接信息 4.编写SQL语句&#xff0c;进行测试 前言 什么是MyBatis? MyBatis是⼀款优秀的 持久层 框架&#xff0c;⽤于简化JDBC的开发 Mybatis操作数据库的入门步骤&#xff1a; 1.创建springboot⼯程 2.数…

kwin- 插件加载绘制流程

1. 配置文件的作用具体是做什么的&#xff1f; 相当于用户强制设置了特效的开关&#xff0c;对于没有写在配置文件里的特效&#xff0c;会检测默认加载值&#xff0c;确定是否加载。写在了文件里的会根据返回的值&#xff0c;来加载特效。 2. 为什么配置文件没有写&#xff0c…

【自用】王道文件管理强化笔记

文章目录 操作系统引导:磁盘初始化文件打开过程角度1文件的打开过程角度2 内存映射的文件访问 操作系统引导: ①CPU从一个特定主存地址开始&#xff0c;取指令&#xff0c;执行ROM中的引导程序(先进行硬件自检&#xff0c;再开机) ②)将磁盘的第一块–主引导记录读入内存&…

【Ubuntu】git

文章目录 1.配置SSH key2. 基础知识操作命令1分支branch 如果对git命令使用不熟悉&#xff0c;推荐一个非常棒的git在线练习工具 Learn Git Branching。 https://m.runoob.com/git/git-basic-operations.html 1.配置SSH key ssh-keygen -t rsa -C "YOUR EMAIL"完成…

markdown 中启用音频支持

markdown 中启用音频支持 markdown 默认不支持音频文件&#xff0c;我们通过 html 标签渲染 flask项目 其中音频文件放在 /static/audios/vad_example.wav markdown 内容如下&#xff1a; ## 音频播放器示例 <audio controls ><source src"vad_example.wav…

Flink源码剖析

写在前面 最近一段时间都没有更新博客了&#xff0c;原因有点离谱&#xff0c;在实现flink的两阶段提交的时候&#xff0c;每次执行自定义的notifyCheckpointComplete时候&#xff0c;好像就会停止消费数据&#xff0c;完成notifyComplete后再消费数据&#xff1b;基于上述原因…

力扣16~20题

题16&#xff08;中等&#xff09;&#xff1a; 思路&#xff1a; 双指针法&#xff0c;和15题差不多&#xff0c;就是要排除了&#xff0c;如果total<target则排除了更小的&#xff08;left右移&#xff09;&#xff0c;如果total>target则排除了更大的&#xff08;rig…

kafka的成神秘籍(java)

kafka的成神秘籍 kafka的简介 ​ Kafka 最初是由Linkedin 即领英公司基于Scala和 Java语言开发的分布式消息发布-订阅系统&#xff0c;现已捐献给Apache软件基金会。Kafka 最被广为人知的是作为一个 消息队列(mq)系统存在&#xff0c;而事实上kafka已然成为一个流行的分布式流…

【mmengine】配置器(config)(进阶)继承与导出,命令行修改配置

一、配置文件的继承 1.1 继承机制概述 新建optimizer_cfg.py: optimizer dict(typeSGD, lr0.02, momentum0.9, weight_decay0.0001)新建runtime_cfg.py: device "cuda" gpu_ids [0, 1] batch_size 64 epochs 100 num_workers 8新建resnet50.py: _base_ […

Vue 路由设置

为了防止遗忘&#xff0c;记录一下用Vue写前端配置路由时的过程&#xff0c;方便后续再需要用到时回忆。 一、举个例子 假如需要实现这样的界面逻辑&#xff1a; 在HomePage中有一组选项卡按钮用于导航到子页面&#xff0c;而子页面Page1中有一个按钮&#xff0c;其响应事件是…

笔记-stm32移植ucos

文章目录 一、UCOS的基础知识1.1 前后台系统:1.2 RTOS系统可剥夺型内核:前后台系统和RTOS系统 1.3 UCOS系统简介学习方法 二、ucossii移植Step1&#xff1a;在工程中建立存放UCOSS代码的文件夹UCOSIIStep2:向CORE文件夹添加文件Step3:向Config文件夹添加文件Step4:向port文件夹…

LLM4Rec最新工作: 字节发布用于序列推荐的分层大模型HLLM

前几个月 Meta HSTU 点燃各大厂商对 LLM4Rec 的热情&#xff0c;一时间&#xff0c;探索推荐领域的 Scaling Law、实现推荐的 ChatGPT 时刻、取代传统推荐模型等一系列话题让人兴奋&#xff0c;然而理想有多丰满&#xff0c;现实就有多骨感&#xff0c;尚未有业界公开真正复刻 …

vscode中配置python虚拟环境

python虚拟环境作用 Python虚拟环境允许你为每个独立的项目创建一个隔离的环境&#xff0c;这样每个项目都可以拥有自己的一套Python安装包和依赖&#xff0c;不会互相影响。实际使用中&#xff0c;可以在vscode或pycharm中使用虚拟环境。 1.创建虚拟环境的方法&#xff1a; …

【NLP自然语言处理】01-基础学习路径简介

目的&#xff1a;让大家能够在 AI-NLP 领域由基础到入门具体安排&#xff1a; NLP介绍 文本预处理RNN 及其变体&#xff08;涉及案例&#xff09;Transformer 原理详解迁移学习 和 Bert 模型详解 &#xff08;涉及案例&#xff09;特点&#xff1a; 原理 实践每个文章会有练习…

04-SpringBootWeb案例(中)

3. 员工管理 完成了部门管理的功能开发之后&#xff0c;我们进入到下一环节员工管理功能的开发。 基于以上原型&#xff0c;我们可以把员工管理功能分为&#xff1a; 分页查询&#xff08;今天完成&#xff09;带条件的分页查询&#xff08;今天完成&#xff09;删除员工&am…