Redis常用概念及操作
概述
是什么?
- 定义:
- 和MySQL、Oracle等数据库的主要区别:
数据库 | 存储的位置 | 数据逻辑结构 |
---|---|---|
MySQL、Oracle等数据库 | 硬盘 | 关系型数据 |
Redis | 内存 | key-value |
- 厉害之处:
- 性能强
- 支持的数据结构多:string、hash、set、list、zset
常见的作用
缓存
分布式锁
- Command
- Lua
- Redission
五种数据结构
常用数据结构:string
、hash
String
常用命令
字符串常用操作
SET key value //存入字符串键值对
MSET key value [key value ...] //批量存储字符串键值对
SETNX key value //存入一个不存在的字符串键值对
GET key //获取一个字符串键值
MGET key [key ...] //批量获取字符串键值
DEL key [key ...] //删除一个键
EXPIRE key seconds //设置一个键的过期时间(秒)
原子加减
INCR key //将key中储存的数字值加1
DECR key //将key中储存的数字值减1
INCRBY key increment //将key所储存的值加上increment
DECRBY key decrement //将key所储存的值减去decrement
应用场景
单值缓存
SET key value
GET key
对象缓存
SET user:1 value(json格式数据)
MSET user:1:name xiaoming user:1:balance 1888
MGET user:1:name user:1:balance
计数器
INCR article:readcount:{文章id}
GET article:readcount:{文章id}
Web集群session共享
- spring session + redis实现session共享
分布式系统全局序列号
INCRBY orderId 1000 //redis批量生成序列号提升性能
Hash
常用命令
HSET key field value //存储一个哈希表key的键值
HSETNX key field value //存储一个不存在的哈希表key的键值
HMSET key field value [field value ...] //在一个哈希表key中存储多个键值对
HGET key field //获取哈希表key对应的field键值
HMGET key field [field ...] //批量获取哈希表key中多个field键值
HDEL key field [field ...] //删除哈希表key中的field键值
HLEN key //返回哈希表key中field的数量
HGETALL key //返回哈希表key中所有的键值
HINCRBY key field increment //为哈希表key中field键的值加上增量increment
应用场景
对象缓存
HMSET user {userId}:name xiaoming {userId}:balance 1888
HMSET user 1:name xiaoming 1:balance 1888
HMGET user 1:name 1:balance
优缺点
优点
- 同类数据归类整合储存,方便数据管理
- 相比string操作消耗内存与cpu更小
- 相比string储存更节省空间
缺点
- 过期功能不能使用在field上,只能用在key上
- Redis集群架构下不适合大规模使用(集群数据分片
->
数据倾斜)
Set
常用命令
Set常用操作
SADD key member [member ...] //往集合key中存入元素,元素存在则忽略,若key不存在则新建
SREM key member [member ...] //从集合key中删除元素
SMEMBERS key //获取集合key中所有元素
SCARD key //获取集合key的元素个数
SISMEMBER key member //判断member元素是否存在于集合key中
SRANDMEMBER key [count] //从集合key中选出count个元素,元素不从key中删除
SPOP key [count] //从集合key中选出count个元素,元素从key中删除
Set运算操作
SINTER key [key ...] //交集运算
SINTERSTORE destination key [key ..] //将交集结果存入新集合destination中
SUNION key [key ..] //并集运算
SUNIONSTORE destination key [key ...] //将并集结果存入新集合destination中
SDIFF key [key ...] //差集运算
SDIFFSTORE destination key [key ...] //将差集结果存入新集合destination中
应用场景
微信抽奖小程序
- 点击参与抽奖加入集合
SADD key {userlD}
- 查看参与抽奖所有用户
SMEMBERS key
- 抽取count名中奖者
SRANDMEMBER key [count] / SPOP key [count]
微信微博点赞,收藏,标签
- 点赞
SADD like:{消息ID} {用户ID}
- 取消点赞
SREM like:{消息ID} {用户ID}
- 检查用户是否点过赞
SISMEMBER like:{消息ID} {用户ID}
- 获取点赞的用户列表
SMEMBERS like:{消息ID}
- 获取点赞用户数
SCARD like:{消息ID}
List
常用命令
LPUSH key value [value ...] //将一个或多个值value插入到key列表的表头(最左边)
RPUSH key value [value ...] //将一个或多个值value插入到key列表的表尾(最右边)
LPOP key //移除并返回key列表的头元素
RPOP key //移除并返回key列表的尾元素
LRANGE key start stop //返回列表key中指定区间内的元素,区间以偏移量start和stop指定
BLPOP key [key ...] timeout //从key列表表头弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
BRPOP key [key ...] timeout //从key列表表尾弹出一个元素,若列表中没有元素,阻塞等待timeout秒,如果timeout=0,一直阻塞等待
Zset
常用命令
ZSet常用操作
ZADD key score member [[score member]…] //往有序集合key中加入带分值元素
ZREM key member [member …] //从有序集合key中删除元素
ZSCORE key member //返回有序集合key中元素member的分值
ZINCRBY key increment member //为有序集合key中元素member的分值加上increment
ZCARD key //返回有序集合key中元素个数
ZRANGE key start stop [WITHSCORES] //正序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES] //倒序获取有序集合key从start下标到stop下标的元素
Zset集合操作
ZUNIONSTORE destkey numkeys key [key ...] //并集计算
ZINTERSTORE destkey numkeys key [key …] //交集计算
应用场景
Zset集合操作实现排行榜
1)点击新闻
ZINCRBY hotNews:20190819 1 守护香港
2)展示当日排行前十
ZREVRANGE hotNews:20190819 0 9 WITHSCORES
3)七日搜索榜单计算
ZUNIONSTORE hotNews:20190813-20190819 7 hotNews:20190813 hotNews:20190814... hotNews:20190819
4)展示七日排行前十
ZREVRANGE hotNews:20190813-20190819 0 9 WITHSCORES
其它命令
Redis事务
比较鸡肋,一般不会使用它,此处仅做了解
没有原子性
redis事务四大指令: MULTI、EXEC、DISCARD、WATCH。这四个指令构成了redis事务处理的基础。
- MULTI用来组装一个事务
- EXEC用来执行一个事务
- DISCARD用来取消一个事务
- WATCH用来监视一些key,一旦这些key在事务执行之前被改变,则取消事务的执行
redis 127.0.0.1:6379> MULTI # 标记事务开始
OK
redis 127.0.0.1:6379> INCR user_id # 多条命令按顺序入队
QUEUED
redis 127.0.0.1:6379> INCR user_id
QUEUED
redis 127.0.0.1:6379> INCR user_id
QUEUED
redis 127.0.0.1:6379> PING
QUEUED
redis 127.0.0.1:6379> EXEC # 执行
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) PONG
有关redis事务,经常会遇到的是两类错误:
-
调用EXEC之前的错误:redis会拒绝执行这一事务
-
127.0.0.1:6379> multi OK 127.0.0.1:6379> haha //一个明显错误的指令 (error) ERR unknown command 'haha' 127.0.0.1:6379> ping QUEUED 127.0.0.1:6379> exec //redis无情的拒绝了事务的执行,原因是“之前出现了错误” (error) EXECABORT Transaction discarded because of previous errors.
-
-
调用EXEC之后的错误:redis不会理睬这些错误,而是继续向下执行事务中的其他命令
-
127.0.0.1:6379> multi OK 127.0.0.1:6379> set age 23 QUEUED //age不是集合,所以如下是一条明显错误的指令 127.0.0.1:6379> sadd age 15 QUEUED 127.0.0.1:6379> set age 29 QUEUED 127.0.0.1:6379> exec //执行事务时,redis不会理睬第2条指令执行错误 1) OK 2) (error) WRONGTYPE Operation against a key holding the wrong kind of value 3) OK 127.0.0.1:6379> get age "29" //可以看出第3条指令被成功执行了
-
管道(Pipeline)
redis客户端执行一条命令分4个过程:发送命令-〉命令排队-〉命令执行-〉返回结果
未使用pipeline执行N条命令:
使用了pipeline执行N条命令:
客户端可以一次性发送多个请求而不用等待服务器的响应,待所有命令都发送完后再一次性读取服务的响应,这样可以极大的降低多条命令执行的网络传输开销,管道执行多条命令的网络开销实际上只相当于一次命令执行的网络开销。需要注意到是用pipeline方式打包命令发送,redis必须在处理完所有命令前先缓存起所有命令的处理结果。打包的命令越多,缓存消耗内存也越多。所以并不是打包的命令越多越好。
pipeline中发送的每个command都会被server立即执行,如果执行失败,将会在此后的响应中得到信息;也就是pipeline并不是表达“所有command都一起成功”的语义,管道中前面命令失败,后面命令不会有影响,继续执行。
使用pipeline组装的命令个数不能太多,不然数据量过大,增加客户端的等待时间,还可能造成网络阻塞,可以将大量命令的拆分多个小的pipeline命令完成。
Lua脚本
Redis在2.6推出了脚本功能,允许开发者使用Lua语言编写脚本传到Redis中执行。使用脚本的好处如下:
-
减少网络开销:本来5次网络请求的操作,可以用一个请求完成,原先5次请求的逻辑放在redis服务器上完成。使用脚本,减少了网络往返时延。这点跟管道类似。
-
原子操作:Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。管道不是原子的,不过redis的批量操作命令(类似mset)是原子的。
-
替代redis的事务功能:redis自带的事务功能很鸡肋,而redis的lua脚本几乎实现了常规的事务功能,官方推荐如果要使用redis的事务功能可以用redis lua替代。
官网文档上有这样一段话:
A Redis script is transactional by definition, so everything you can do with a Redis transaction, you can also do with a script, and usually the script will be both simpler and faster.
示例:
jedis.set("product_stock_10016", "15"); //初始化商品10016的库存
String script = " local count = redis.call('get', KEYS[1]) " +
" local a = tonumber(count) " +
" local b = tonumber(ARGV[1]) " +
" if a >= b then " +
" redis.call('set', KEYS[1], a-b) " +
" return 1 " +
" end " +
" return 0 ";
Object obj = jedis.eval(script, Arrays.asList("product_stock_10016"), Arrays.asList("10"));
System.out.println(obj);
注意,不要在Lua脚本中出现死循环和耗时的运算,否则redis会阻塞,将不接受其他的命令, 所以使用时要注意不能出现死循环、耗时的运算。redis是单进程、单线程执行脚本。管道不会阻塞redis。
Spring使用Redis
Java连接开发工具
- Jedis
- lettuce
- 目前流行的redis集成到java环境有
jedis
和lettuce
。在之前版本中jedis可能更受青睐。但是在springboot2.0之后官方推荐的默认就是lettuce连接。因为jedis数据池虽然线程安全,但是占用的资源较大,而lettuce基于netty和nio相关实现,性能更加强悍,占用的资源也比较少,并且使用起来也不难
- 目前流行的redis集成到java环境有
Springboot使用Redis
RedisTemplate
和StringRedisTemplate
- Redis数据库连接池:lettuce
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.daheww.demo</groupId>
<artifactId>demo-redis</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo-redis</name>
<description>demo-redis</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
RedisConfig.java
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.core.json.JsonReadFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.text.SimpleDateFormat;
import java.util.TimeZone;
/**
* @author daheww
* @date 2022/10/27
*/
@Configuration
@SuppressWarnings("all")
public class RedisConfig {
public final static String TIME_ZONE = "GMT+8";
public final static String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
@Bean
public RedisTemplate<String, Object> redisCacheTemplate(RedisConnectionFactory redisConnectionFactory) {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = getJackson2JsonRedisSerializer();
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// 为了开发方便,一般都是<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// 设置键(key)的序列化采用StringRedisSerializer。
template.setKeySerializer(stringRedisSerializer);
// 设置值(value)的序列化采用jackson的序列化。
template.setValueSerializer(jackson2JsonRedisSerializer);
// 设置hash键(key)的序列化采用StringRedisSerializer。
template.setHashKeySerializer(stringRedisSerializer);
// 设置hash值(value)的序列化采用jackson的序列化。
template.setHashValueSerializer(jackson2JsonRedisSerializer);
return template;
}
private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() {
// JSON序列化的细节配置
ObjectMapper om = new ObjectMapper();
om.setTimeZone(TimeZone.getTimeZone(TIME_ZONE));
om.configure(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature(), true);
// 设置全局的日期转换的格式
om.setDateFormat(new SimpleDateFormat(DATETIME_FORMAT));
// 实体转json时,属性值为NULL不参加序列化
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
jackson2JsonRedisSerializer.setObjectMapper(om);
return jackson2JsonRedisSerializer;
}
@Bean
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
// StringRedisTemplate和RedisTemplate:
// 1.StringRedisTemplate继承RedisTemplate
// 2.区别主要在于他们使用的序列化类:
// RedisTemplate默认使用的是JdkSerializationRedisSerializer(存入数据会将数据先序列化成字节数组然后在存入Redis数据库)
// StringRedisTemplate使用的是StringRedisSerializer
// 使用时注意事项:
// 当你的redis数据库里面本来存的是字符串数据或者你要存取的数据就是字符串类型数据的时候,那么你就使用StringRedisTemplate即可。
// 但是如果你的数据是复杂的对象类型,而取出的时候又不想做任何的数据转换,直接从Redis里面取出一个对象,那么使用RedisTemplate是更好的选择。
// 构建StringRedisTemplate
StringRedisTemplate stringTemplate = new StringRedisTemplate();
stringTemplate.setConnectionFactory(redisConnectionFactory);
stringTemplate.afterPropertiesSet();
return stringTemplate;
}
}