Redis-基础篇
Redis是一个开源、高性能、内存键值存储数据库,由 Salvatore Sanfilippo(网名antirez)创建,并在BSD许可下发布。它不仅可以用作缓存系统来加速数据访问,还可以作为持久化的主数据存储系统或消息中间件使用。Redis因其数据结构丰富、性能优异和高可用性而被广泛应用在现代分布式架构中。
目录
一、认识Redis
1.1、认识NoSQL
1.2、认识Redis
1.3、安装Redis
二、Redis的常见命令
2.1、5种常见的数据结构
2.2、通用命令
2.3、不同数据结构的操作命令
三、Redis的Java客户端
3.1、Jedis客户端
3.2、SpringDataRedis客户端
一、认识Redis
1.1、认识NoSQL
SQL:结构化、表有关联、支持SQL查询、满足事务的ACID、存储在磁盘、垂直扩展、适用于数据结构固定,相关业务的数据安全性与一致性要求高的场合
NoSQL:非结构、无关联的、非SQL、满足基本一致性、存储在内存、水平扩展、适用于数据结构不固定,对一致性与安全性要求不高,但是对性能要求较高的场合
常见的NoSQL有:键值对类型的Redis、文档类型的MongoDB
1.2、认识Redis
Redis( Remote Dictionary Server)的全程是远程字典服务器,是一个基于内存的非关系型数据库。
redis的特征:
1.键值对类型的数据库,支持丰富的数据类型
2.单线程,每个命令具备原子性
3.低延迟,速度快(基于内存、IO多路复用、良好的编码)
4.支持数据的持久化
5.支持主从集群与分片集群
6.支持多语言客户端
1.3、安装Redis
这个不过多介绍,直接下载安装即可。
安装redis服务器和客户端,并安装redis可视化管理工具Another Redis Desktop Manager。
二、Redis的常见命令
2.1、5种常见的数据结构
redis是一个键值对类型的数据库,key的类型一般是String,value的类型就多种多样:
基本数据类型:
String类型:缓存用户信息、计数器(例如点赞数、浏览量)、简单的键值对存储等。
Hash类型:存储用户属性集合(如用户的姓名、年龄、地址等)、产品详情等多字段数据结构。
List类型:存储用户属性集合(如用户的姓名、年龄、地址等)、产品详情等多字段数据结构。
Set类型:适用于标签系统(给一篇文章打上多个标签)、唯一事件记录等。
SortedSet类型:跳跃表提供O(log N)级别的插入、删除和查找操作,并按分数排序。
特殊数据类型:
GEO类型:Geo数据类型允许用户存储地理位置信息,并执行地理半径查询、邻近点搜索等操作。
BitMap类型:用于用户在线状态跟踪、访问统计(例如用户是否读过某篇文章)。
HyperLog类型:日活用户统计、网站独立访客统计、广告点击去重等需要估算大量唯一元素数量而不需精确存储所有元素的场景。
2.2、通用命令
通用命令常见的有:
KEYS:查看符合模板的所有key,不建议在生产环境上使用
DEL:删除一个指定的key
EXISTS:判断一个key是否存在
EXPIRE:为一个key设置有效期,有效期到了key会自动删除
TTL:查看key的剩余有效期
2.3、不同数据结构的操作命令
String类型:字符串类型,包括普通字符串,整数,浮点数
API如下:
SET:添加或者修改string类型的键值对
GET:根据key获取string类型的value
MSET:批量添加多个string类型的键值对
MGET:根据string类型的key获取多个string类型的值
INCR:让整型的key自增1
INCREBY:整数设置步长的自增
INCREBYFLOAT:按照指定步长的浮点型自增
SETNX:添加一个string类型的键值对,前提是key不存在,否则不执行
SETEX:添加一个string类型的键值对,并指定有效期
redis的key允许有多个单词形成层级结构,多个单词用“:”隔开,如果value是一个Java对象,则可以将对象序列化成JSON字符串后存储:
例如key可以为 项目名:业务名:类型:id value为{“id”:1,"product":"小米手机","price":"2999"}
这样redis会根据冒号:进行层级划分。
Hash类型:也称为散列,value是一个无序字典,类似于Java中的HashMap结构。之前的string类型的value是将对象序列化成JSON字符串后存储,当需要修改某个字段时很不方便。
Hash结构可以将每个字段独立存储,可以针对每个字段进行操作。
相关的API:
HSET key field value:添加或者修改hash类型的key的field值
HGET key field:获取一个hash类型的key的field值
HMSET:批量添加多个hash类型key的field值
HMGET:批量查询多个hash类型key的field值
HGETALL:获取一个hash类型key的所有feild的值
HKEYS:获取一个hash类型的key中的所有feild
HVALS:获取一个hash类型的key中的所有value
HINCREBY:让hash类型的key自增并指定步长
HSETNX:添加一个hash类型的field,前提是field不存在,否则不执行
List类型:List类型与Java中的LinkedList类似,可以看作是一个双向链表,支持正向与反向检索。
特征:有序、允许元素重复、插入删除快、查询速度一般
List的常见命令如下:
LPUSH key element...:向列表左侧插入一个或者多个元素
LPOP key :移除并返回列表左侧额第一个元素,没有则返回nil
RPUSH key element...:向列表右侧插入一个或者多个元素
RPOP key:移除并返回列表右侧的第一个元素
LRANGE key start end:返回一段角标范围内的所有元素
BLPOP与BRPOP:移除指定的元素,没有元素时并设置等待时间,而不是直接返回nil
List模拟栈:lpush与lpop rpush与rpop
List模拟队列:lpush与rpop
List模拟阻塞队列:blpop与brpop
Set类型:Redis中的set与Java中的HashSet类似,具有如下特征:无序、元素不可重复、查找快
、支持交集、并集差集等操作。
SET类型的常见命令:
SADD key member...:向set中添加一个或者多个元素
SREM key member...:向set中移除指定元素
SCARD key:返回set中的元素个数
SISMEMBER key member:判断一个元素是否在set中
SMEMBERS:获取set中的所有元素
SINTER key1 key2:求两个key的交集
SDIFF:求两个集合的差集
SUNION:求两个集合的并集
SortedSet类型:有序的集合,每个元素都带有一个score属性,可以根据score属性进行排序,底层是一个跳表+hash表的实现。跳跃表提供O(log N)级别的插入、删除和查找操作,并按分数排序;hash表用于快速查找成员的存在性。
跳表是通过随机函数维护平衡性的,当我们在跳表中插入数据的时候,我们通过选择同时将这个数据插入到部分索引层中,如何选择索引层,可以通过一个随机函数来决定这个节点插入到哪几级索引中,比如随机生成了k,那么就将这个索引加入到,第一级到第k级索引中。
SortedSet具有以下特点:
1、可排序 2、元素不重复 3、查询速度快
常见的SortedSort的api如下:默认是升序,如果向降序前缀由Z改成ZREV
三、Redis的Java客户端
3.1、Jedis客户端
常见的Redis的Java客户端有Jedis、Lettuce、Redisson三种,具体如下。
下面我们通过jedis客户端连接redis服务器,并进行单元测试,具体如下:
1.首先添加三方依赖。
redis.clients jedis 3.7.0 org.junit.jupiter junit-jupiter 5.7.0 test
2.编写单元测试模块,jedis客户端连接redis服务器,并进行crud基本操作,最后释放连接。
import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import java.util.Map; /** * @author nuist__NJUPT * @ClassName JedisTest * @description: jedis测试类 * @date 2024/03/02 */ public class JedisTest { private Jedis jedis ; @BeforeEach void setUp(){ // 1.建立连接 jedis = new Jedis("localhost", 6379) ; // 2.设置密码 jedis.auth("123456") ; // 3.选择库 jedis.select(0) ; } @Test void testString(){ // 存入数据 String result = jedis.set("name", "mandy"); System.out.println(result); // 获取数据 String name = jedis.get("name"); System.out.println("name : " + name ); } @Test void testHash(){ // 存值 jedis.hset("user:1", "name", "jack") ; jedis.hset("user:1", "age", "21") ; // 取值 Map stringStringMap = jedis.hgetAll("user:1"); System.out.println(stringStringMap); } @AfterEach void tearDown(){ if(jedis != null){ jedis.close(); } } }
jedis本身是线程不安全的,而且频繁的创建与销毁jedis连接会有性能损耗,因此推荐使用jedis连接池的方式代替直连的方式。
1.定义一个连接池工具类,用于建立jedis连接,并返回jedis对象,jedis使用完放回连接池而不是直接销毁。
import redis.clients.jedis.Jedis; import redis.clients.jedis.JedisPool; import redis.clients.jedis.JedisPoolConfig; /** * @author nuist__NJUPT * @ClassName JedisConnectionFactory * @description: Jedis连接池 * @date 2024/03/02 */ public class JedisConnectionFactory { private static final JedisPool jedisPool ; static { // 配置连接池 JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxTotal(8); jedisPoolConfig.setMaxIdle(8); jedisPoolConfig.setMinIdle(0); jedisPoolConfig.setMaxWaitMillis(1000); // 创建连接池对象 jedisPool= new JedisPool(jedisPoolConfig, "localhost", 6379, 1000, "123456"); } public static Jedis getJedis(){ return jedisPool.getResource() ; } }
2.客户端直接通过连接池获取jedis对象就可以,不用直接newjedis对象进行直连了。
import com.alibaba.jedis.util.JedisConnectionFactory; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import redis.clients.jedis.Jedis; import java.util.Map; /** * @author nuist__NJUPT * @ClassName JedisTest * @description: jedis测试类 * @date 2024/03/02 */ public class JedisTest { private Jedis jedis ; @BeforeEach void setUp(){ // 1.建立连接 // jedis = new Jedis("localhost", 6379) ; jedis = JedisConnectionFactory.getJedis() ; // 2.设置密码 jedis.auth("123456") ; // 3.选择库 jedis.select(0) ; } @Test void testString(){ // 存入数据 String result = jedis.set("name", "mandy"); System.out.println(result); // 获取数据 String name = jedis.get("name"); System.out.println("name : " + name ); } @Test void testHash(){ // 存值 jedis.hset("user:1", "name", "jack") ; jedis.hset("user:1", "age", "21") ; // 取值 Map stringStringMap = jedis.hgetAll("user:1"); System.out.println(stringStringMap); } @AfterEach void tearDown(){ if(jedis != null){ jedis.close(); } } }
3.2、SpringDataRedis客户端
SpringData是Spring中数据操作的模块,包含了对多种数据库的集成,其中对redis的集成就是SpringDataRedis。它提供了对不同redis客户端的整合(jedis、Lettuce),提供了RedisTemplate统一API来操作Redis,支持redis的发布订阅模型,支持redis哨兵和redis集群,支持基于Lettuce的响应式编程,支持序列化与反序列化。
下面看一下SpringDataRedis提供的工具类RedisTemplate的应用,首先创建springboot项目并导入redis依赖。
com.fasterxml.jackson.core jackson-databind 2.13.5 org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2
然后在yml文件中进行配值,配值redis的数据源信息。
spring: redis: host: 127.0.0.1 port: 6379 password: 123456 lettuce: pool: max-active: 8 max-idle: 8 min-idle: 0 max-wait: 100
编写redisTemplate的配值类,防止在redis在接收Object类型时,把Object对象序列化成字节形式,变成一串乱码,可读性差,占用内存。
/** * @author nuist__NJUPT * @ClassName RedisConfig * @description: redis配置类 * @date 2024/03/02 */ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializer; @Configuration public class RedisConfig { /** * RedisTemplate可以接收任意Object作为值写入Redis, * 只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化,得到的一串很长的值 * 缺点:可读性查、浪费存储空间 */ @Bean public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){ // 1.创建 redisTemplate RedisTemplate redisTemplate = new RedisTemplate(); // 2.设置连接工厂 redisTemplate.setConnectionFactory(redisConnectionFactory); // 3.设置序列化工具 GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); // key 采用 String 序列化 redisTemplate.setKeySerializer(RedisSerializer.string()); redisTemplate.setHashKeySerializer(RedisSerializer.string()); // value 采用 json 序列化 redisTemplate.setValueSerializer(jsonRedisSerializer); redisTemplate.setHashValueSerializer(jsonRedisSerializer); return redisTemplate; } }
定义一个实体类。
import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @author nuist__NJUPT * @ClassName User * @description: 实体类 * @date 2024/03/02 */ @Data @NoArgsConstructor @AllArgsConstructor public class User { private String name ; private Integer age ; }
最后编写单元测试模块,利用redisTemplate进行测试。
import com.alibaba.redisdemo.pojo.User; 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 class RedisDemoApplicationTests { @Autowired private RedisTemplate redisTemplate; @Test void testString(){ // 添加一个元素 redisTemplate.opsForValue().set("name", "Jack"); // 获取元素 Object object = redisTemplate.opsForValue().get("name"); System.out.println(object); } @Test void testSaveUser(){ //写入数据 redisTemplate.opsForValue().set("user:2", new User("wang", 18)); // 读取数据 User user = (User) redisTemplate.opsForValue().get("user:2"); System.out.println(user); } @Test void testHash(){ stringRedisTemplate.opsForHash().put("user:3","name", "liu"); stringRedisTemplate.opsForHash().put("user:3","age","18") ; Map entries = stringRedisTemplate.opsForHash().entries("user:3"); System.out.println("entries = " + entries); } }
上述尽管Json序列化的方式满足要求,但是会发现仍然存在一些问题,比如JSON序列化器将类型class写入了json中,存入redis会导致额外的内存开销。
为了节省内存空间,不使用JSON序列化来处理value,而是处理String序列化器,要求只能存储String类型的key与value,当存储Java对象的时候需要手动的序列化与反序列化。
可以采用如下写法:
@Autowired private StringRedisTemplate stringRedisTemplate ; private static final ObjectMapper mapper = new ObjectMapper() ; @Test void testSave() throws JsonProcessingException { // 创建对象 User user = new User("wang", 18); // 手动序列化 String s = mapper.writeValueAsString(user); //写入数据 stringRedisTemplate.opsForValue().set("user:2",s); // 读取数据 String user1 = stringRedisTemplate.opsForValue().get("user:2"); // s手动反序列化 User user2 = mapper.readValue(user1, User.class); System.out.println(user2); }
使用fastJson进行序列化与反序列化也可以,需要添加依赖。
@Autowired private StringRedisTemplate stringRedisTemplate ; @Test void testSave() throws JsonProcessingException { // 创建对象 User user = new User("wang", 19); // 手动序列化 String s = JSON.toJSONString(user) ; //写入数据 stringRedisTemplate.opsForValue().set("user:2",s); // 读取数据 String user1 = stringRedisTemplate.opsForValue().get("user:2"); // s手动反序列化 User user2 = JSON.parseObject(user1, User.class) ; System.out.println(user2); }
添加fastJson依赖。
com.alibaba.fastjson2 fastjson2 2.0.32
总结:redis中有两种序列化方式,推荐使用第二种。
1.第一种是自定义RedisTemplate,修改其序列化器,相对方便,但是写入redis会存class对象,占用额外的内存空间。
2.使用StringRedisTemplate,默认使用String序列化器,写入redis需要将Java对象手动序列化为json,读取redis需要将读取到的json反序列化为Java对象。