1、事务
Redis事务本质:一组命令的集合,一个事务中的所有命令都会被序列化,并且会按顺序执行。分为三个阶段:
- 开启事务(multi)
- 命令入队
- 执行事务(exec)
(1)正常执行事务
127.0.0.1:6379> MULTI #开启事务
OK
127.0.0.1:6379> set k1 v1 #命令入队
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> set k3 v3
QUEUED
127.0.0.1:6379> EXEC #执行事务
1) OK
2) OK
3) "v2"
4) OK
(2)放弃事务
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> set v3 k3
QUEUED
127.0.0.1:6379> DISCARD #取消事务
OK
127.0.0.1:6379> get v1
(nil)
(3)命令有错
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> settt k3 v3 #写一个错误命令
(error) ERR unknown command `settt`, with args beginning with: `k3`, `v3`,
127.0.0.1:6379> set k4 v4
QUEUED
127.0.0.1:6379> EXEC
(error) EXECABORT Transaction discarded because of previous errors. ##所有命令都不会执行
127.0.0.1:6379> get k2
(nil)
(4)语法性错误
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> set k1 v1
QUEUED
127.0.0.1:6379> INCR k1 # 给一个字符串加1
QUEUED
127.0.0.1:6379> set k2 v2
QUEUED
127.0.0.1:6379> get k2
QUEUED
127.0.0.1:6379> exec ##虽然出现了错误,但事务依然执行成功
1) OK
2) (error) ERR value is not an integer or out of range
3) OK
4) "v2"
127.0.0.1:6379> get k2
"v2"
乐观锁
乐观锁:认为什么时候都不多出问题,所以不会上锁,只需要更新数据的时候去判断一下,在此期间是否有人修改过这个数据。
(1)单线程操作过程中没有人修改过这个数据
127.0.0.1:6379> set money 100
OK
127.0.0.1:6379> WATCH money #监听这个money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 20 #操作money
QUEUED
127.0.0.1:6379> exec
1) (integer) 80 # 执行成功
(2)多线程中间发生了修改
#### 客户端1
127.0.0.1:6379> set money 1000
OK
127.0.0.1:6379> watch money
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> DECRBY money 200
QUEUED
127.0.0.1:6379> exec #执行失败
(nil)
#### 客户端2
127.0.0.1:6379> get money
"1000"
127.0.0.1:6379> DECRBY money 300 #事务执行之前修改数据
(integer) 700
##### 事务执行失败 可以先解锁
127.0.0.1:6379> UNWATCH #解锁
OK
127.0.0.1:6379> WATCH money #再次加锁 获取的是最新的值
OK
127.0.0.1:6379> MULTI
OK
127.0.0.1:6379> decrby money 200
QUEUED
127.0.0.1:6379> exec
1) (integer) 500
2、使用Jedis操作Redis
Jedis就是使用Java操作Redsi的中间件。
导入依赖
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.3.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>
测试
//创建jedis对象
Jedis jedis = new Jedis("127.0.0.1", 6379);
//中的方法和linux中的命令都一样,不在演示
System.out.println(jedis.ping());
事务
public static void main(String[] args) {
//创建jedis对象
Jedis jedis = new Jedis("127.0.0.1", 6379);
System.out.println(jedis.ping());
jedis.flushDB();
Transaction multi = jedis.multi();//开启事务
try {
multi.set("name","zhang");
int i=1/0 ;
multi.exec();//执行事务
} catch (Exception e) {
multi.discard();//取消事务
e.printStackTrace();
}finally {
System.out.println(jedis.get("name"));
jedis.close();//关闭连接
}
}
3、SpringBoot集成Redis
在SpringBoot 2之后,原来使用的Jedis被替换为了lettuce。
- Jedis:采用的是直连,多线程操作是不安全的,使用jedis pool连接池,类似于BIO
- lettuce:采用netty,实例可以在多个线程中进行共享,不存在线程不安全的情况,类似于NIO
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
简单测试
package com.xupt;
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.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@SpringBootTest
class DemoApplicationTests {
@Autowired
private RedisTemplate redisTemplate;
@Test
void contextLoads() {
/*基本操作:
* opsForValue()操作字符串
* opsForList()操作List
* opsForSet()操作Set
* opsForZSet()操作Zset
* opsForHash()操作Hash
* */
redisTemplate.opsForValue().set("name","zhangsan");
System.out.println(redisTemplate.opsForValue().get("name"));
/*常用的操作可以直接进行*/
//获取redis的连接对象
RedisConnection connection = redisTemplate.getConnectionFactory().getConnection();
connection.flushDb();
}
}
源码探析
@Bean
@ConditionalOnMissingBean( name = {"redisTemplate"}) //我们可以自己定义一个redisTemplate来替换这个默认的
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
//默认的RedisTemplate没有过多的设置,redis对象都需要序列化
//要将类型设置为<String,Object>
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
//由于String是redis中最常使用的类型,单独给String提供了一个方法来方便操作
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
#######################################
//序列化配置
private RedisSerializer keySerializer = null;
@Nullable
private RedisSerializer valueSerializer = null;
@Nullable
private RedisSerializer hashKeySerializer = null;
@Nullable
private RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
@Nullable
...
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (this.defaultSerializer == null) {
//默认的序列化方式jdk让字符串进行转译,可以采用json方式
this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
}
if (this.enableDefaultSerializer) {
if (this.keySerializer == null) {
this.keySerializer = this.defaultSerializer;
defaultUsed = true;
.....
没有序列化时,传递对象
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
//在实际开发中,实体类都会序列化
public class User implements Serializable {
private String name;
private int age;
}
自定义RedisTemplate
@Configuration
public class RedisConfig {
//编写自己的redisTemplate
@Bean
public RedisTemplate<String, Object> RedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
//json的序列化配置
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
//String的序列化
StringRedisSerializer redisSerializer = new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(serializer);
//value采用string的序列化方式
template.setValueSerializer(serializer);
//Hash的key采用string的序列化方式
template.setHashKeySerializer(serializer);
//Hash的value采用string的序列化方式
template.setHashValueSerializer(serializer);
template.afterPropertiesSet();
return template;
}
}
测试
@Configuration
public class RedisConfig {
//编写自己的redisTemplate
@Bean
public RedisTemplate<String, Object> RedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
//json的序列化配置
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
serializer.setObjectMapper(objectMapper);
//String的序列化
StringRedisSerializer redisSerializer = new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(redisSerializer);
//value采用string的序列化方式
template.setValueSerializer(redisSerializer);
//Hash的key采用string的序列化方式
template.setHashKeySerializer(redisSerializer);
//Hash的value采用string的序列化方式
template.setHashValueSerializer(redisSerializer);
template.afterPropertiesSet();
return template;
}
}
结果对比
4、redis.conf配置文件解析
网络配置
bind 127.0.0.1 # 绑定ip
protected-mode no #保护模式
port 6379 #端口号
通用配置
daemonize yes # 以守护进程的方式运行,默认是no,我们使用时需要将修改为yes
pidfile /var/run/redis_6379.pid #进程pid文件
####日志信息
# Specify the server verbosity level.
# This can be one of:
# debug (a lot of information, useful for development/testing)
# verbose (many rarely useful info, but not a mess like the debug level)
# notice (moderately verbose, what you want in production probably) ## 生产环境使用
# warning (only very important / critical messages are logged)
loglevel notice # 默认
logfile "" #日志的文件位置命
databases 16 #数据库的数量
always-show-logo yes #是否总是显示logo
快照(rad持久化)
## 持久化条件
save 900 1 # 900秒内至少有一个key进行了修改,进行持久化
save 300 10 # 300秒内至少有10个key进行了修改,进行持久化
save 60 10000 # 60秒内至少有100000个key进行了修改,进行持久化
stop-writes-on-bgsave-error yes # 持久化出错,是否继续工作
rdbcompression yes #是否压缩rdb文件
rdbchecksum yes #保存rdb文件是进行错误校验
dbfilename dump.rdb # rdb文件保存的目录
主从复制(REPLICATION )
安全(SECURITY)
# requirepass foobared # 设置连接密码,也可以通过命令进行,默认没有密码
限制
# maxclients 10000 # 设置最大客户端连接数
# maxmemory <bytes> # redis配置的最大内存容量
# maxmemory-policy noeviction # 内存到达上限之后的处理策略
1、默认为 noeviction :这个策略是说如果redis数据库达到最大内存时会不进行置换key,但是会返回给客户端一个错误信息
2、volatile-lru:对生存周期内很少有使用key进行置换
3、volatile-random:对生存周期中的key进行随机置换
4、volatile-ttl:对生存周期内的key随机进行抽取,在这个抽取中取出生存周期最不常用的key进行置换
5、allkeys-random:对整个数据库的key进行随机置换
6、allkeys-lru:置换整个数据库中最少使用的key
APPEND ONLY MODE(aof持久化)
appendonly no #默认采用的是rdb方式持久化,aof是关闭的,大部分情况下,rdb完全够用
appendfilename "appendonly.aof" #rof持久化文件
# appendfsync always #每次修改都会持久化
appendfsync everysec #每秒执行一次,默认
# appendfsync no #不执行
5、持久化
Redis是内存数据库,如果不将内存中的数据库保存到磁盘,那么一旦服务器进程退出,数据就会丢失,所以Redis提供了持久化的功能。
(1) RDB(Redis DataBase)
是什么?
- 在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是将快照文件直接读到内存里
- Redis会单独创建(fork)一个子进程来进行持久化,会先将数据写入到 一个临时文件中,待持久化过程都结束了,再用这个临时文件替换上次持久化好的文件。 整个过程中,主进程是不进行任何IO操作的,这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。
Fork的作用是复制一个与当前进程一样的进程。新进程的所有数据(变量、环境变量、程序计数器等) 数值都和原进程一致,但是是一个全新的进程,并作为原进程的子进程
如何触发RDB快照?
1.自动
- save规则满足时,系统会自从生成RDB快照
- 执行flushall命令,但里面是空的,没有意义;
- 执行shutdown退出时
2.手动(save和bgsave两种方式)
- save:SAVE 直接调用 rdbSave函数 ,阻塞 Redis 主进程,直到保存完成为止。在主进程阻塞期间,服务器不能处理客户端的任何请求。如果数据量小,用此命令可能感觉不出有什么区别,但是当数据量很大的时候,就需要谨慎使用这个命令。
- BGSAVE 命令执行之后立即返回 OK ,然后 Redis fork 出一个新子进程,原来的 Redis 进程(父进程)继续处理客户端请求,而子进程则负责将数据保存到磁盘,然后退出。
BGSAVE方式比较适合线上的维护操作。
如何从rdb中恢复数据?
只需要将rdb文件放在我们redis启动目录就可以,redis启动的时候会自动检查rdb并恢复数据
查看需要存在的位置
redis 127.0.0.1:6379> CONFIG GET dir
1) "dir"
2) "/usr/local/redis/bin"
优点:
1 适合大规模的数据恢复。
2 如果业务对数据完整性和一致性要求不高,RDB是很好的选择。
缺点:
1 数据的完整性和一致性不高,因为RDB可能在最后一次备份时宕机了。
2 备份时占用内存,因为Redis 在备份时会独立创建一个子进程,将数据写入到一个临时文件(此时内存中的数据是原来的两倍哦),最后再将临时文件替换之前的备份文件。所以要考虑到大概两倍的数据膨胀性。
(2)AOF(Append Only File)
是什么?
以日志的形式来记录每个写操作,将Redis执行过程的所有写指令记录下来(读操作不记录),只许追加文件但不可以改写文件,redis启动之初会读取该文件重新构建数据,换言之,redis重启的话就会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
测试
appendonly yes # 将no改为yes,aof才能生效
然后我们给redis里面set几个值,然后就有了appendonly.aof,打开这个文件可以看到存的都是一些写命令和数据。
但是当我们认为破坏这个文件时,随便加一些其他字符,我们发现redis无法启动了,需要使用redis-check-aof --fix来修复我们的文件。
[root@yao bin]# redis-check-aof --fix appendonly.aof
0x 8b: Expected prefix '*', got: 'q'
AOF analyzed: size=149, ok_up_to=139, diff=10
This will shrink the AOF from 149 bytes, with 10 bytes, to 139 bytes
Continue? [y/N]: y
Successfully truncated AOF
### 修复成功之后,发现他将我们加的删除了,然后就可以启动redis
重写
1、重写:
AOF采用文件追加方式,文件会越来越大,为避免出现此种情况,新增了重写机制,当AOF文件的大小超过所设定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof。
2、重写原理
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件最后再rename), 遍历新进程的内存中数据,每条记录有一条的Set语句。重写aof文件的操作,并没有读取旧的aof文件, 而是将整个内存中的数据库内容用命令的方式重写了一个新的aof文件,这点和快照有点类似。
3、触发机制
Redis会记录上次重写时的AOF大小,默认配置是当AOF文件大小是上次rewrite后大小的一倍且文件大于64M时触发。
优点
- appendfsync always 同步持久化 每次发生数据变更会被立即记录到磁盘 性能较差但数据完整性比较好。
- appendfsync everysec 异步操作,每秒记录 如果一秒内宕机,会发生数据丢失。
- appendfsync no 从不同步
缺点
- 相同数据集的数据而言aof文件要远大于rdb文件,恢复速度慢于rdb
- Aof运行效率要慢于rdb,每秒同步策略效率较好,不同步效率和rdb相同
6、消息订阅发布
进程间的一种消息通信模式:发送者(pub)发送消息,订阅者(sub)接收消息。
(1)是什么?
当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 这个消息就会被发送给订阅它的三个客户端:
(2)测试
客户端1
127.0.0.1:6379> SUBSCRIBE redis # 订阅一个频道
Reading messages... (press Ctrl-C to quit)
1) "subscribe"
2) "redis"
3) (integer) 1
1) "message" # 消息
2) "redis" # 频道信息
3) "nihao" # 消息内容
1) "message"
2) "redis"
3) "lalala"
客户端2
127.0.0.1:6379> PUBLISH redis nihao # 向频道中发送消息
(integer) 1
127.0.0.1:6379> PUBLISH redis lalala
(integer) 1