Redis事务
-
ACID,原子性,一致性,隔离性,持久性
-
但Redis单条命令是原子性的,但Redis事务不保证原子性,没有隔离性
-
Redis事务本质:一组命令的集合,一个事务中的所有命令会被序列化,在事务执行的过程中,会按照顺序执行
- 一次性、顺序性、排它性
-
Redis事务分三个阶段:
- 开启事务 (multi)
- 命令入队 (…)
- 执行事务 (exec)
-
开启事务后命令不会立即执行,而是在执行事务的时候一块执行
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED # 进入队列 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> get k2 QUEUED 127.0.0.1:6379(TX)> set k3 v3 QUEUED 127.0.0.1:6379(TX)> exec # 执行事务 1) OK 2) OK 3) "v2" 4) OK
-
放弃事务,一旦放弃,事务中的所有命令都不会执行
127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> set k4 v4 QUEUED 127.0.0.1:6379(TX)> DISCARD # 放弃事务 OK 127.0.0.1:6379> get k4 (nil)
-
编译型异常
# 编译型异常(代码有问题,命令有错),事务中所有的命令都不会被执行 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> getset k3 # 此处出现编译型异常,命令写法有误 (error) ERR wrong number of arguments for 'getset' command 127.0.0.1:6379(TX)> set k5 v5 QUEUED 127.0.0.1:6379(TX)> exec # 执行事务发现错误 (error) EXECABORT Transaction discarded because of previous errors. 127.0.0.1:6379> get k5 # 发现事务中的命令没有执行 (nil)
-
运行时异常
# 运行时异常(1/0),如果队列中存在语法性错误,那么执行命令的时候,除去错误语句,其他命令是可以正常执行的,所以没有原子性 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> set k1 v1 QUEUED 127.0.0.1:6379(TX)> incr k1 # 创造运行时异常,语法没错,但字符串无法自增,会出现运行时异常 QUEUED 127.0.0.1:6379(TX)> set k2 v2 QUEUED 127.0.0.1:6379(TX)> set k3 v3 QUEUED 127.0.0.1:6379(TX)> get k3 QUEUED 127.0.0.1:6379(TX)> exec # 执行发现除了错误命令其他命令正常执行 1) OK 2) (error) ERR value is not an integer or out of range 3) OK 4) OK 5) "v3"
监控
悲观锁
- 很悲观,认为什么时候都会出问题,无论做什么都会加锁,影响性能
乐观锁
-
很乐观,认为什么时候都不会出问题,所以不会上锁,更新数据的时候判断一下在此期间这个数据是否修改过。用一个version字段来处理,可以实现redis乐观锁操作
- 获取version
- 更新的时候比较version
-
Redis监视测试
# 正常执行成功 127.0.0.1:6379> watch money # watch监视money OK 127.0.0.1:6379> multi # 开启事务 OK 127.0.0.1:6379(TX)> DECRBY money 20 QUEUED 127.0.0.1:6379(TX)> INCRBY out 20 QUEUED 127.0.0.1:6379(TX)> exec # 事务正常执行 1) (integer) 80 2) (integer) 20 # 在事务提交前money就被修改了,使用watch可以实现redis乐观锁操作 127.0.0.1:6379> watch money # 监视money,去拿money的version OK 127.0.0.1:6379> multi OK 127.0.0.1:6379(TX)> DECRBY money 10 QUEUED 127.0.0.1:6379(TX)> incrby out 10 QUEUED ( # 另一个客户端操作 127.0.0.1:6379> get money "80" 127.0.0.1:6379> set money 1000 OK ) 127.0.0.1:6379(TX)> exec # 事务提交失败,所有命令不予执行。 (nil) # 取消检测命令。事务结束或者取消,无论是否成功,监视自动取消 127.0.0.1:6379> unwatch OK # watch相当于内置版本号,每次修改都有记录,所以不存在ABA问题
Jedis
-
我们要使用java来操作redis
-
什么是Jedis?
- Jedis是Redis官方推荐的java连接开发工具
- 使用java操作Redis的中间件
-
测试
-
导入依赖
<!--导入jedis包--> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>3.3.0</version> </dependency> <!--fastjson--> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.75</version> </dependency>
-
编码测试
-
连接数据库
public class TestPing { public static void main(String[] args) { // 1. new Jedis对象 Jedis jedis = new Jedis("192.168.88.22", 6379); // jedis所有的命令就是之前我们学习的所有指令 System.out.println(jedis.ping()); // 输出PONG表示成功连接 } }
-
操作命令…
-
断开连接:
jedis.close();
-
-
事务
-
实现事务
public class TestTX { public static void main(String[] args) { Jedis jedis = new Jedis("192.168.88.22", 6379); JSONObject jsonObject = new JSONObject(); jsonObject.put("hello","world"); jsonObject.put("name","cbc"); // 开启事务 Transaction multi = jedis.multi(); String result = jsonObject.toJSONString(); try { multi.set("user1",result); multi.set("user2",result); multi.exec(); // 执行事务 } catch (Exception e) { multi.discard(); // 放弃事务 e.printStackTrace(); } finally { System.out.println(jedis.get("user1")); System.out.println(jedis.get("user2")); jedis.close(); } } }
-
乐观锁实现
public class TestTX extends Thread{ @Override public void run() { Jedis jedis = new Jedis("192.168.88.22", 6379); jedis.set("money","10"); jedis.close(); } public static void main(String[] args) { TestTX testTX = new TestTX(); Jedis jedis = new Jedis("192.168.88.22", 6379); // 开启事务 jedis.set("money","100"); jedis.watch("money"); Transaction multi = jedis.multi(); testTX.start(); // 开启修改线程 try { Thread.yield(); // 让修改线程先跑 sleep(200); // 顺便休息一下保证修改线程跑完 } catch (InterruptedException e) { e.printStackTrace(); } try { multi.set("money", "1"); System.out.println(multi.exec()); //打印null表示乐观锁成功生效,命令没有执行 } catch (Exception e) { multi.discard(); // 放弃事务 e.printStackTrace(); } finally { System.out.println(jedis.get("money")); jedis.close(); } } }
SpringBoot整合
-
SpringBoot操作数据:Spring-Data
-
Spring-Data也是和SpringBoot齐名的项目
-
说明:在springboot2.x之后,原来使用的jedis被替换为了lettuce
- jedis:采用的是直连,多个线程操作的话是不安全的,如果想要避免不安全,就得使用jedis pool连接池,会导致其他问题,更像BIO模式
- lettuce:采用netty,实例可以在多个线程中共享,不存在线程不安全的情况,可以减少线程数量,更像NIO模式
-
测试
-
导入依赖
<!--操作redis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
-
配置连接
spring: # 配置redis redis: host: 192.168.88.22 port: 6379
-
测试
@Test void contextLoads() { // opsForValue 操作字符串 String BitMap // opsForList 操作List // opsForSet 操作Set // opsForHash 操作Hash // opsForZSet 操作ZSet // opsForGeo 操作Geo // opsForHyperLogLog 操作HyperLogLog // 除了以上基本的操作,我们常用的方法都可以直接通过redisTemplate点出来,比如事务和基本的CRUD // 获取redis的连接对象 //RedisConnection connection = redisTemplate.getConnectionFactory().getConnection(); //connection.flushDb(); redisTemplate.opsForValue().set("mykey","你好"); System.out.println(redisTemplate.opsForValue().get("mykey")); }
-
-
默认的RedisTemplate没有过多的设置,redis对象都是需要序列化的
-
默认的序列化方式是JDK序列化,会让我们的字符串转义,我们可能会使用json来序列化,就需要自己定义配置类
@Test void contextLoads() throws JsonProcessingException { // 真实开发一般都使用json来传递对象 User user = new User("你好", 8); String s = new ObjectMapper().writeValueAsString(user); // 序列化,不序列化无法传输,也可以使用pojo类序列化 redisTemplate.opsForValue().set("user", s); System.out.println(redisTemplate.opsForValue().get("user")); }
-
pojo类序列化
// 在企业中,我们的所有 pojo 类一般都会序列化 public class User implements Serializable { private String name; private int age;
自定义template模板
@Configuration
public class RedisConfig {
// 编写我们自己的redisTemplate
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 为了开发方便,一般直接使用<String, Object>
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
// json序列化配置
Jackson2JsonRedisSerializer Jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,
ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
Jackson2JsonRedisSerializer.setObjectMapper(mapper);
// String 的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
//key采用string的序列化方式
template.setKeySerializer(stringRedisSerializer);
//value采用jackson的序列化方式
template.setValueSerializer(Jackson2JsonRedisSerializer);
//hash采用string的序列化方式
template.setHashKeySerializer(stringRedisSerializer);
//hash的value采用jackson的序列化方式
template.setHashKeySerializer(Jackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
-
Redis工具类
- 在真实开发中,一般都可以看到一个公司自己封装的RedisUtil
-
使用工具类
@Test void contextLoads() { redisUtil.set("user","cbc"); System.out.println(redisUtil.get("user")); }
Redis.conf详解
-
启动的时候,就需要配置文件启动
-
容量单位不区分大小写,G和GB有区别,可以使用 include 组合多个配置问题,就像我们spring的import标签
-
网络配置
# 指定 redis 只接收来自于该IP地址的请求,如果不进行设置,那么将处理所有请求 bind 127.0.0.1 # 是否开启保护模式,默认开启。要是配置里没有指定bind和密码。开启该参数后,redis只会本地进行访问,拒绝外部访问。 # 要是开启了密码和bind,可以开启。否则最好关闭,设置为no protected-mode yes # redis监听的端口号 port 6379 # 此参数确定了TCP连接中已完成队列(完成三次握手之后)的长度, 当然此值必须不大于Linux系统定义的 # /proc/sys/net/core/somaxconn值,默认是511,而Linux的默认参数值是128。当系统并发量大并且客 # 户端速度缓慢的时候,可以将这二个参数一起参考设定。该内核参数默认值一般是128,对于负载很大的服 # 务程序来说大大的不够。一般会将它修改为2048或者更大。在/etc/sysctl.conf中添加:net.core.somaxconn = 2048, # 然后在终端中执行sysctl -p tcp-backlog 511 # 此参数为设置客户端空闲超过timeout,服务端会断开连接,为0则服务端不会主动断开连接,不能小于0 timeout 0 # tcp keepalive参数。如果设置不为0,就使用配置tcp的SO_KEEPALIVE值,使用keepalive有两个好处: # 检测挂掉的对端。降低中间设备出问题而导致网络看似连接却已经与对端端口的问题。在Linux内核中, # 设置了keepalive,redis会定时给对端发送ack。检测到对端关闭需要两倍的设置值 tcp-keepalive 300 # 是否在后台执行,yes:后台运行;no:不是后台运行 daemonize yes # redis的进程文件 pidfile /var/run/redis/redis.pid # 指定了服务端日志的级别。级别包括: # debug(很多信息,方便开发、测试) # verbose(许多有用的信息,但是没有debug级别信息多) # notice(适当的日志级别,适合生产环境) # warn(只有非常重要的信息) loglevel notice # 指定了记录日志的文件。空字符串的话,日志会打印到标准输出设备。后台运行的redis标准输出是/dev/null logfile /usr/local/redis/var/redis.log # 是否打开记录syslog功能 # syslog-enabled no # syslog的标识符。 # syslog-ident redis # 日志的来源、设备 # syslog-facility local0 # 数据库的数量,默认使用的数据库是16。可以通过"SELECT 【数据库序号】"命令选择一个数据库,序号从0开始 databases 16
-
安全配置
- 命令配置密码:
config set requirepass "123456"
- 登录:
auth 123456
- redis6.0以后建议使用acl用户代替密码了
# requirepass配置可以让用户使用AUTH命令来认证密码,才能使用其他命令。这让redis可以使用在不受信任的网络中。 # 为了保持向后的兼容性,可以注释该命令,因为大部分用户也不需要认证。使用requirepass的时候需要注意, # 因为redis太快了,每秒可以认证15w次密码,简单的密码很容易被攻破,所以最好使用一个更复杂的密码 # requirepass cbcbcbc # 把危险的命令给修改成其他名称。比如CONFIG命令可以重命名为一个很难被猜到的命令,这样用户不能使用,而内部工具还能接着使用 # rename-command CONFIG b840fc02d524045429941cc15f59e41cb7be6c52 # 设置成一个空的值,可以禁止一个命令 # rename-command CONFIG ""
- 命令配置密码:
-
快照配置
# RDB核心规则配置 save <指定时间间隔> <执行指定次数更新操作>,满足条件就将内存中的数据同步到硬盘中。 # 官方出厂配置默认是900秒内有1个更改,300秒内有10个更改以及60秒内有10000个更改,则将内存中的数据快照写入磁盘。 # 若不想用RDB方案,可以把 save "" 的注释打开,下面三个注释 # save "" save 900 1 save 300 10 save 60 10000 # 当RDB持久化出现错误后,是否依然进行继续进行工作,yes:不能进行工作,no:可以继续进行工作, # 可以通过info中的rdb_last_bgsave_status了解RDB持久化是否有错误 stop-writes-on-bgsave-error yes # 配置存储至本地数据库时是否压缩数据,默认为yes。Redis采用LZF压缩方式,但占用了一点CPU的时间。 # 若关闭该选项,但会导致数据库文件变的巨大。建议开启。 rdbcompression yes # 是否校验rdb文件;从rdb格式的第五个版本开始,在rdb文件的末尾会带上CRC64的校验和。 # 这跟有利于文件的容错性,但是在保存rdb文件的时候,会有大概10%的性能损耗, # 所以如果你追求高性能,可以关闭该配置 rdbchecksum yes # 指定本地数据库文件名,一般采用默认的 dump.rdb dbfilename dump.rdb # 数据目录,数据库的写入会在这个目录。rdb、aof文件也会写在这个目录 dir /usr/local/redis/var
-
主从复制配置
# 复制选项,slave复制对应的master。 # replicaof <masterip> <masterport> # 如果master设置了requirepass,那么slave要连上master,需要有master的密码才行。 # masterauth就是用来配置master的密码,这样可以在连上master后进行认证。 # masterauth <master-password> # 当从库同主机失去连接或者复制正在进行,从机库有两种运行方式: # 1) 如果slave-serve-stale-data设置为yes(默认设置),从库会继续响应客户端的请求。 # 2) 如果slave-serve-stale-data设置为no,INFO,replicaOF, AUTH, PING, SHUTDOWN, REPLCONF, ROLE, # CONFIG, SUBSCRIBE, UNSUBSCRIBE,PSUBSCRIBE, PUNSUBSCRIBE, PUBLISH, PUBSUB,COMMAND, POST, # HOST: and LATENCY命令之外的任何请求都会返回一个错误”SYNC with master in progress” replica-serve-stale-data yes # 作为从服务器,默认情况下是只读的(yes),可以修改成NO,用于写(不建议) # replica-read-only yes # 是否使用socket方式复制数据。目前redis复制提供两种方式,disk和socket。如果新的slave连上来或者 # 重连的slave无法部分同步,就会执行全量同步,master会生成rdb文件。有2种方式:disk方式是master创建 # 一个新的进程把rdb文件保存到磁盘,再把磁盘上的rdb文件传递给slave。socket是master创建一个新的进 # 程,直接把rdb文件以socket的方式发给slave。disk方式的时候,当一个rdb保存的过程中,多个slave都能 # 共享这个rdb文件。socket的方式就的一个个slave顺序复制。在磁盘速度缓慢,网速快的情况下推荐用socket方式。 # repl-diskless-sync no # diskless复制的延迟时间,防止设置为0。一旦复制开始,节点不会再接收新slave的复制请求直到下一个rdb传输。 # 所以最好等待一段时间,等更多的slave连上来 repl-diskless-sync-delay 5 # slave根据指定的时间间隔向服务器发送ping请求。时间间隔可以通过 repl_ping_slave_period 来设置,默认10秒。 # repl-ping-slave-period 10 # 复制连接超时时间。master和slave都有超时时间的设置。master检测到slave上次发送的时间超过repl-timeout, # 即认为slave离线,清除该slave信息。slave检测到上次和master交互的时间超过repl-timeout,则认为master离线。 # 需要注意的是repl-timeout需要设置一个比repl-ping-slave-period更大的值,不然会经常检测到超时 # repl-timeout 60 # 是否禁止复制tcp链接的tcp nodelay参数,可传递yes或者no。默认是no,即使用tcp nodelay。 # 如果master设置了yes来禁止tcp nodelay设置,在把数据复制给slave的时候,会减少包的数量和更小的网络带宽。 # 但是这也可能带来数据的延迟。默认我们推荐更小的延迟,但是在数据量传输很大的场景下,建议选择yes repl-disable-tcp-nodelay no # 复制缓冲区大小,这是一个环形复制缓冲区,用来保存最新复制的命令。这样在slave离线的时候,不需要完 # 全复制master的数据,如果可以执行部分同步,只需要把缓冲区的部分数据复制给slave,就能恢复正常复制状态。 # 缓冲区的大小越大,slave离线的时间可以更长,复制缓冲区只有在有slave连接的时候才分配内存。 # 没有slave的一段时间,内存会被释放出来,默认1m # repl-backlog-size 1mb # master没有slave一段时间会释放复制缓冲区的内存,repl-backlog-ttl用来设置该时间长度。单位为秒。 # repl-backlog-ttl 3600 # 当master不可用,Sentinel会根据slave的优先级选举一个master。最低的优先级的slave当选master。 # 而配置成0,永远不会被选举 replica-priority 100 #redis提供了可以让master停止写入的方式,如果配置了min-replicas-to-write,健康的slave的个数小于N,mater就禁止写入。 # master最少得有多少个健康的slave存活才能执行写命令。这个配置虽然不能保证N个slave都一定能接收到master的写操作, # 但是能避免没有足够健康的slave的时候,master不能写入来避免数据丢失。设置为0是关闭该功能 # min-replicas-to-write 3 # 延迟小于min-replicas-max-lag秒的slave才认为是健康的slave # min-replicas-max-lag 10 # 设置1或另一个设置为0禁用这个特性。 # Setting one or the other to 0 disables the feature. # By default min-replicas-to-write is set to 0 (feature disabled) and # min-replicas-max-lag is set to 10.
-
客户端配置
# 设置能连上redis的最大客户端连接数量。默认是10000个客户端连接。由于redis不区分连接是客户端连接还 # 是内部打开文件或者和slave连接等,所以maxclients最小建议设置到32。如果超过了maxclients,redis会给 # 新的连接发送’max number of clients reached’,并关闭连接 # maxclients 10000
-
内存配置
# redis配置的最大内存容量。当内存满了,需要配合maxmemory-policy策略进行处理。 # 注意slave的输出缓冲区是不计算在maxmemory内的。所以为了防止主机内存使用完, # 建议设置的maxmemory需要更小一些 maxmemory 122000000 # 内存容量超过maxmemory后的处理策略: # volatile-lru:利用LRU算法移除设置过过期时间的key # volatile-random:随机移除设置过过期时间的key # volatile-ttl:移除即将过期的key,根据最近过期时间来删除(辅以TTL) # allkeys-lru:利用LRU算法移除任何key # allkeys-random:随机移除任何key # noeviction:不移除任何key,只是返回一个写错误 # 上面的这些驱逐策略,如果redis没有合适的key驱逐,对于写命令,还是会返回错误。redis将不再接收写请求,只接收get请求 # 写命令包括:set setnx setex append incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd # sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby zunionstore zinterstore hset # hsetnx hmset hincrby incrby decrby getset mset msetnx exec sort # maxmemory-policy noeviction # lru检测的样本数。使用lru或者ttl淘汰算法,从需要淘汰的列表中随机选择sample个key,选出闲置时间最长的key移除 # maxmemory-samples 5 # 是否开启salve的最大内存 # replica-ignore-maxmemory yes
-
AOF配置
# Redis 默认不开启。它的出现是为了弥补RDB的不足(数据的不一致性),所以它采用日志的形式来记录每个写操作, # 并追加到文件中。Redis 重启的会根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作 # 默认redis使用的是rdb方式持久化,这种方式在许多应用中已经足够用了。 # 但是redis如果中途宕机,会导致可能有几分钟的数据丢失,根据save来策略进行持久化, # Append Only File是另一种持久化方式,可以提供更好的持久化特性。 # Redis会把每次写入的数据在接收后都写入 appendonly.aof 文件, # 每次启动时Redis都会先把这个文件的数据读入内存里,先忽略RDB文件。若开启rdb则将no改为yes appendonly no # 指定本地数据库文件名,默认值为 appendonly.aof appendfilename "appendonly.aof" # aof持久化策略的配置 # no表示不执行fsync,由操作系统保证数据同步到磁盘,速度最快 # always表示每次写入都执行fsync,以保证数据同步到磁盘 # everysec表示每秒执行一次fsync,可能会导致丢失这1s数据 # appendfsync always appendfsync everysec # appendfsync no # 在aof重写或者写入rdb文件的时候,会执行大量IO,此时对于everysec和always的aof模式来说, # 执行fsync会造成阻塞过长时间,no-appendfsync-on-rewrite字段设置为默认设置为no。如果对延迟要求很高的 # 应用,这个字段可以设置为yes,否则还是设置为no,这样对持久化特性来说这是更安全的选择。 # 设置为yes表示rewrite期间对新写操作不fsync,暂时存在内存中,等rewrite完成后再写入,默认为no,建议yes。 # Linux的默认fsync策略是30秒。可能丢失30秒数据 no-appendfsync-on-rewrite no # aof自动重写配置。当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写,即当aof文件 # 增长到一定大小的时候Redis能够调用bgrewriteaof对日志文件进行重写。当前AOF文件大小是上次日志重写得 # 到AOF文件大小的二倍(设置为100)时,自动启动新的日志重写过程 auto-aof-rewrite-percentage 100 # 设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写 auto-aof-rewrite-min-size 64mb # aof文件可能在尾部是不完整的,当redis启动的时候,aof文件的数据被载入内存。重启可能发生在redis所 # 在的主机操作系统宕机后,尤其在ext4文件系统没有加上data=ordered选项(redis宕机或者异常终止不会造 # 成尾部不完整现象。)出现这种现象,可以选择让redis退出,或者导入尽可能多的数据。如果选择的是yes, # 当截断的aof文件被导入的时候,会自动发布一个log给客户端然后load。 # 如果是no,用户必须手动redis-check-aof修复AOF文件才可以 aof-load-truncated yes # 加载redis时,可以识别AOF文件以"redis"开头。 # 字符串并加载带前缀的RDB文件,然后继续加载AOF尾巴 aof-use-rdb-preamble yes