目录
Redis的简单使用补充说明
可视化操作
https://github.com/lework/RedisDesktopManager-Windows/releases
下载安装就行
转码
redis-cli --raw
可以将UTF-8编码字符转换成具体的内容
数据类型
String类型
String类型,也就是字符串类型,redis中最简单的类型。
value是字符串。但是会根据字符串的格式不同,可以将分为3类:
- String:普通字符串
- int:整数类型,可以自增、自减操作
- float:浮点类型,可以自增、自减操作
常用操作
- set:添加或修改
- get:根据key获取
- mset:批量添加/修改多个
- mget:获取多个
- incr:整型key自增1
- incrby:整型自增指定步长->incrby age 2
- incrbyfloat:让一个浮点类型自增并指定步长
- setnx:当且仅当key不存在的时候添加
- setex:添加并指定有效期(秒)
Hash类型
Hash类型,也叫散列,value是一个无序字典,类似Java的HashMap;
String结构是将对象序列化为Json字符串,当需要修改对象某个字段时不方便,Hash结构可以将每个对象中的每个字段独立存储,可以针对单个字段做CRUD;
常用操作
- hset key field value:添加或修改key的field的值
- hget key field:获取一个key的field的值
- hmset:批量添加/修改key的多个field的值
- hmget:批量获取key的多个field的值
- hgetall:获取一个key的所有field和value
- hkeys:获取key的所有field
- hvals:获取key的所有value
- hincrbu:让key的field自增并指定步长
- hsetnx:当且仅当key的field值不存在才添加
List类型
Redis中的L类型与Java中的LinkedList类似,可以看作是一个双向链表结构。支持正向/反向检索;
特征也与LinkedLi类似:
- 有序
- 元素可以重复
- 插入和删除快,查询速度一般
常用操作
- lpush/rpush key element…:向列表左/右侧插入一个或多个元素
- lpop/rpop key:移除并返回链表左/右侧第一个元素,没有返回nil
- lrange key start end:返回一段范围内的所有元素
- blpop/brpop:与lpop/rpop类似,在没有元素时等待指定时间,不会直接返回nil
Set类型
Redis的Set结构与Java中的HashSet类似,可以看作是 一个value为null的HashMap。因为也是一个hash表,因此具备与HashSet类似的特征:
- 无序
- 元素不可重复
- 查找快
- 支持交集、并集、差集等功能
常用操作
- sadd key member…:向set中添加一个或多个元素
- srem key member…:移除set中指定元素
- scard key:返回set中元素的个数
- sismember key member:判断一个元素是否在set中
- smembers:获取set中所有元素
- sinter key1 key2…:求key1与key2的交集
- sdiff key1 key2…:求key1与key2的差集,前有后没有的
- sunion key1 key2…:求key1和key2的并集
SortedSet类型
Redis的SortedSet是一个可排序的set集合,与Java中的TreeSet类似,但底层数据结构缺差别很大,SortedSet的每一个元素都带有一个score属性,可以基于score属性对元素排序,底层的实现是一个跳表(SkipList)加Hash表。SortedSet具备下列特性:
- 可排序
- 元素不重复
- 查询速度快
因为SortedSet的可排序特性,经常用来实现排行榜这样的功能;
常用操作
- zadd key score member:添加一个或多个元素,如果已经存在则更新score值
- zrem key member:删除一个指定元素
- zscore key member:获取指定元素的score值
- zrank key member:获取指定元素的排名
- zcard key:获取元素个数
- zcount key min max:统计score值在给定范围内的所有元素的个数
- zincrby key increment member:让指定元素自增指定步长
- zrange key min max:按照score排序后,获取指定排名范围内的元素
- zrangebyscore key min max:按照score排序后,获取指定score范围内的元素
- zdiff、zinter、zunion:求差集、交集、并集
排名有关的可以反序,zrevxxx
Java客户端
各个常用客户端比较:
- Jedis:以redis命令作为方法名称,学习成本低,简单实用。但是Jedis实例是线程不安全的,多线程环境下需要基于连接池来使用;
- Lettuce:Lettuce是基于Netty实现的,支持同步、异步和响应式编程方式,而且是线程安全的。支持Redis的哨兵模式、集群模式和管道模式;
- Redisson:Redisson是一个基于Redis实现的分布时、可伸缩的Java数据结构集合。包含了诸如Map、Queue、Lock、Semaphore、AtomicLone等强大功能;
Jedis
简单demo
引入依赖
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.7.0</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.7.0</version>
<scope>test</scope>
</dependency>
</dependencies>
测试代码
package per.xgt.demo;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import redis.clients.jedis.Jedis;
/**
* @Author: gentao9527
* @date: 2022/8/31 17:07
* @Description: TODO
* @Version: 1.0
*/
public class JedisTest {
private Jedis jedis;
@BeforeEach
void set(){
// 建立连接
jedis = new Jedis("ip",port);
// 密码或者可以输入账号密码
jedis.auth("username","password");
// 选择库
jedis.select(0);
}
@Test
public void test(){
jedis.set("k1","v1");
System.out.println(jedis.get("k1"));
}
@AfterEach
public void afterTest(){
if (null != jedis){
jedis.close();
}
}
}
只用Jedis,方法名与Redis命令一致。
使用完后,需要释放资源。
JedisPool
Jedis本身是线程不安全的,并且频繁地创建和销毁连接会有性能损耗,因此推荐使用Jedis连接池代替Jedis的直连方式;
测试代码:
package per.xgt.demo;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
/**
* @Author: gentao9527
* @date: 2022/8/31 17:26
* @Description: TODO
* @Version: 1.0
*/
public class JedisPoolTest {
private static JedisPool jedispool;
static {
// 配置连接处
JedisPoolConfig config = new JedisPoolConfig();
// 最大连接数
config.setMaxTotal(20);
// 最大空闲连接
config.setMaxIdle(5);
// 最小空闲连接
config.setMinIdle(5);
// 连接等待,没有连接资源时,等待时间,-1表示没有限制,单位毫秒
config.setMaxWaitMillis(1000);
// 创建连接处对象
jedispool = new JedisPool(config,"ip",port,2000,"username","password");
}
public static Jedis getJedis(){
return jedispool.getResource();
}
public static void main(String[] args) {
Jedis jedis = getJedis();
System.out.println(jedis.ping());
}
}
SpringDataRedis
SpringData是Spring中数据操作的模块,包含堆各种数据库的集成,其中Redis的集成模块叫做SpringDataRedis;
- 提供了对不同Redis客户端的整合(Lettuce和Jedis)
- 提供了RedisTemplate统一API来操作Redis
- 支持Redis的发布订阅模式
- 支持Redis的哨兵和Redis集群
- 支持基于Lettuce的响应式编程
- 支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化
- 支持基于Redis的JDKCollection实现
SpringDataRedis中提供了RedisTemplaye工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作封装到了不同的类型中:
- redisTemplate.opsForValue():操作String
- redisTemplate.opsForHash():操作hash
- redisTemplate.opsForList():操作List
- redisTemplate.opsForSet():操作Set
- redisTemplate.opsForZSet():操作SortedSet
- redisTemplate:通用的命令
SpringBoot
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--jackson依赖,如果是有mvc就不需要-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件
spring:
redis:
host: ip
port: 6379
password: password
database: 0
username: username
jedis:
pool:
max-active: 8
max-idle: 8
min-idle: 1
max-wait: 1000ms
测试代码
package per.xgt;
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 Redis05SpringDataTemplateApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
redisTemplate.opsForValue().set("k1","v1");
System.out.println(redisTemplate.opsForValue().get("k1"));
}
}
SpringDataRedis的序列化方式
RedisTemplaye可以接受任意Object作为值写入Redis,只不过写入前会把Object序列化为字节形式,默认是采用JDK序列化;
缺点:
- 可读性差
- 内存占用较大
自定义序列化,自定义RedisTemplate
package per.xgt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
/**
* @Author: gentao9527
* @date: 2022/8/31 20:10
* @Description: TODO
* @Version: 1.0
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
// 设置key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
// 返回
return redisTemplate;
}
}
或
@Bean
public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory connectionFactory){
// 创建RedisTemplate对象
RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 创建JSON序列化工具
Jackson2JsonRedisSerializer jsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// json序列化设置
ObjectMapper mapper = new ObjectMapper();
// 哪些方法规则可以序列化:ALL表示所有方法 ANY表示任何访问修饰符
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
jsonRedisSerializer.setObjectMapper(mapper);
// 设置key的序列化
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
// 设置value的序列化
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
// 返回
return redisTemplate;
}
问题:
在存入对象时候,会有额外开销,将类的class类型写入json结果中,存入Redis;如果去掉,又不能自动实现反序列化
127.0.0.1:6379> get user:1
{"@class":"per.xgt.entity.User","name":"大哥","age":12}
解决:
存储Java对象时,手动完成对象的序列化和反序列化;
Spring默认提供了一个StringRedisTemplate类,key和value的序列化方式默认就是String方式。省去了自定义RedisTemplate的过程;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
void contextLoadsss() throws JsonProcessingException {
// json序列化工具
ObjectMapper mapper = new ObjectMapper();
// 实体类
User user = new User("大哥", 12);
// json转换
String userString = mapper.writeValueAsString(user);
// 存入redis
stringRedisTemplate.opsForValue().set("user:1",userString);
// 取出数据
String stringUser = stringRedisTemplate.opsForValue().get("user:1");
User resultUser = mapper.readValue(stringUser, User.class);
System.out.println(resultUser);
}
Spring配置
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-3.0.xsd">
<context:property-placeholder location="classpath:*.properties"/>
<!--设置数据池-->
<bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
<property name="maxIdle" value="${redis.maxIdle}"></property>
<property name="minIdle" value="${redis.minIdle}"></property>
<property name="maxTotal" value="${redis.maxTotal}"></property>
<property name="maxWaitMillis" value="${redis.maxWaitMillis}"></property>
<property name="testOnBorrow" value="${redis.testOnBorrow}"></property>
</bean>
<!--链接redis-->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.host}"></property>
<property name="port" value="${redis.port}"></property>
<property name="password" value="${redis.password}"></property>
<property name="poolConfig" ref="poolConfig"></property>
</bean>
<bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate" p:connection-factory-ref="connectionFactory" >
<!--以下针对各种数据进行序列化方式的选择,可以自定义一个序列化工具-->
<property name="keySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="valueSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="hashKeySerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
<property name="hashValueSerializer">
<bean class="org.springframework.data.redis.serializer.StringRedisSerializer" />
</property>
</bean>
</beans>
Redisson
缓存穿透
客户端请求的数据在缓存中和数据库中都不存在,缓存永远不会击中,所以请求都会查询数据库;
常见解决方案:
- 缓存空对象
优点:实现简单,维护方便;
缺点:额外的内存消耗,可能造成短期的不一致; - 布隆过滤
优点:内存占用较少,没有多余key;
缺点:实现复杂,存在误判可能;
缓存雪崩
同一时段大量的缓存key同时失效或Redis服务宕机,导致大量请求请求数据库;
常见解决方案:
- 给不同的key的TTL添加随机值
- 利用Redis集群提高服务的可用性
- 给缓存业务添加降级限流策略
- 给业务添加多级缓存
缓存击穿
热点key问题,一个被高并发访问并且缓存重建业务比较复杂的key突然失效,无数的请求访问会在瞬间给数据库带来巨大冲击;
常见解决方案:
- 互斥锁
优点:没有额外的没存消耗,保证一致性,实现简单;
缺点:线程需要等待,性能受影响,可能有死锁的风险; - 逻辑过期
优点:线程无需等待,性能较好;
缺点:不保证一致性,有额外内存消耗,实现复杂;
全局唯一ID
为了增加ID的安全性,可以不直接使用redis自增的数值,而是拼接其他信息:
例:
ID = 符号位1+时间戳21+序列号32
符号位:1bit,永远为0;
时间戳:31bit,以秒为单位,可以使用很长时间;
序列号:32bit:秒内的计数器,支持每秒产生2^32个不同ID;
package per.xgt.jedis;
import redis.clients.jedis.Jedis;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
/**
* @Author: gentao9527
* @date: 2022/9/2 13:41
* @Description: TODO
* @Version: 1.0
*/
public class IDUtil {
// 初始时间戳
private static final Long BEGIN_TIMESTAMP;
// Jedis
private static final Jedis JEDIS;
static {
LocalDateTime time = LocalDateTime.of(2022, 1, 1, 0, 0, 0);
// 时区
long timeSecond = time.toEpochSecond(ZoneOffset.UTC);
BEGIN_TIMESTAMP = timeSecond;
Jedis jedis = new Jedis("ip",6379);
jedis.auth("password");
JEDIS = jedis;
}
public static long nextId(String keyPrefix){
// 生成时间戳
LocalDateTime now = LocalDateTime.now();
long nowTime = now.toEpochSecond(ZoneOffset.UTC);
long timestamp = nowTime - BEGIN_TIMESTAMP;
// 获取当前日期,精确到天
String date = now.format(DateTimeFormatter.ofPattern("yyyyMMdd"));
// 生成序列号
long count = JEDIS.incr("incr:" + keyPrefix + ":" + date);
// 拼接ID
return timestamp << 32 | count;
}
public static void main(String[] args) {
long sssss = nextId("sssss");
System.out.println(sssss);
}
}
Lua
执行redis命令
redis.call(‘命令名称’,‘key’,‘其他参数’,…);
如果要执行脚本:
EVAL "return redis.call('set','haha','hehe')" 0
0:代表需要的key类型的参数个数
如果脚本中的key、value不想写死,可以作为参数传递,key类型参数会放入keys数组,其他参数会放在argv数组中。
注意: 在lua中数组下表从1开始
EVAL "return redis.call('set',KEYS[1],ARGV[1])" 1 haha hehe
redis中解决比对+删除原子性操作Lua脚本,例:
# 获取比对数据
local id = redis.call('get',KEYS[1])
# 比较数据是否一致
if(id == ARGV[1]) then
# 删除
return redis.call('del',KEYS[1])
end
return 0
使用RedisTemplate调用Lua脚本
public class TestLua {
private static final DefaultRedisScript<Long> TEST;
static {
TEST = new DefaultRedisScript<>();
TEST.setLocation(new ClassPathResource("test.lua"));
TEST.setResultType(Long.class);
}
public void eqAndDel(RedisTemplate redisTemplate,String key,String value){
redisTemplate.execute(TEST, Collections.singletonList(key), value);
}
}
setnx的问题
基于setnx实现的分布式锁存在以下问题:
- 不可重入:同一个线程无法多次获取同一把锁;
- 不可重试:获取锁只能尝试一次就返回false,没有重试机制;
- 超时释放:锁超时释放虽然可以避免死锁,但如果业务执行耗时较长,也会导致锁释放,存在安全隐患;
- 主从一致性:如果Redis提供了主从集群,主从同步存在延迟,当主宕机时,并没有将锁同步进从,从而让其他线程趁虚而入;
Redisson
依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.13.6</version>
</dependency>
配置类
@Configuration
public class RedissonConfig {
@Bean
public RedissonClient redissonClient(){
// 配置
Config config = new Config();
config.useSingleServer().setUsername("default").setPassword("XGTflh1314").setAddress("119.3.230.11");
return Redisson.create(config);
}
}
消息队列
基于List消息队列
list数据结构是一个双向链表,很容易模拟队列效果;
队列是入口和出口不在一边,因此可以利用:LPUSH和RPOP、或者RPUSH和LPOP来实现;
注意:当队列中没有消息时,RPOP或LPOP操作会返回null,并不会项JVM的阻塞队列那样会阻塞并等待消息,因此应该使用BRPOP或者BLPOP来实现阻塞效果;
优点:
- 基于Redis的持久化机制,数据安全性有保证
- 可以满足消息有序性
缺点:
- 无法避免消息丢失
- 只支持单消费者
基于PubSub消息队列
Redis2.0引入的消息传递模型。消费者可以订阅一个或多个channel,生产者向对应channel发送消息后,所有订阅者都能收到相关消息;
- SUBSCRIBE CHANNEL:订阅一个或多个频道
- PUBLISH CHANNEL MSG:向一个频道发送消息
- PSUBSCRIBE PATTERN:订阅与pattern格式匹配的所有频道
优点:
- 采用发布订阅模型,支持多生产、多消费
缺点:
- 不支持数据持久化
- 无法避免消息丢失
- 消息堆积有上限,超出时数据丢失
基于Stream消息队列
Stream是Redis5.0引入的一种新数据类型,可以实现一个功能非常完善的消息队列;
-
xadd key [nomkstream] [maxlen|minid [=|~] threshold] [limit count]] * | id field value [field value…]:发送消息。
nomkstream:如果队列不存在,是否自动创建队列,默认是自动创建;
[maxlen|minid [=|~] threshold] [limit count]]:设置消息队列的最大消息数量;
*| id:消息的唯一id,*代表由redis自动生成,格式是“时间戳-递增数字”;
field value:发送到消息队列中的消息; -
xread [count count] [block] streams key [key…] id [id…]:读取消息;
[count count]:每次读取消息的最大数量;
[block]:当没有消息时,是否阻塞,阻塞时长;
streams key:要从哪个队列读取消息,key就是队列名;
id [id…]:起始id,只返回大于该ID的消息,0表示从第一个消息开始,$表示从最新的消息开始;
read的问题:当我们指定起始为id为$时,有可能漏读消息;
消费者组
Consumer Group:将多个消费者分到一个组中,坚挺同一个队列;
- 消息分流:队列中的消息会分流给组内的不同消费者,而不是重复消费,从而加快消息处理的速度;
- 消息标示:消费者组会维护一个标示,记录最后一个被处理的消息,哪怕消费者宕机重启,还会从标示之后读取消息。确保每一个消息都会被消费;
- 消息缺人:消费者获取消息后,消息出于pending的状态,并存入一个pending-list。当处理完成后需要通过xack来确定消息,标记消息为已处理,才会从pending-list移除;
操作:
-
xgroup create key groupname id [mkstream]:创建消费者组;
id:起始id标示,$代表队列中队友一个,0代表队列中第一个;
mkstream:队列不存在时自动创建队列; -
xgroup destory key groupname:删除指定消费者组
-
xgroup create connsumer key groupname consumername:给指定的消费者组添加消费者
-
xgroup delconsumer key groupname consumername:删除消费者组中的指定消费者
-
xreadgroup GROUP group consumer[Count count] [Block] [Noack] streams key… id…:从消费者组读取消息;
group:组名;
consumer:消费者名,如果不存在,会自动创建一个消费者;
Count:查询的最大数量;
Block:当没有消息时,最长等待时间;
Noack:无需手动ACK,获取到消息后自动确认;
key:指定队列名称
id:获取消息的起始id:
>:从下一个未消费的消息开始;
其他:根据其他id开始;