默认端口6379
默认16个数据库,初始默认使用0号库
使用select 切换数据库
统一密码管理,所有库密码相同
dbsize:查看当前库key的数量
flushdb:清空当前库
flushall:清空全部库
redis是单线程 + 多路IO复用技术
串行 vs 多线程+锁(memcached) vs 单线程+多路复用(redis)
这里的数据类型指value数据类型,key类型都是字符串
String是Redis最基本的类型 ,一个key对应一个value。是二进制安全的。意味着Redis的string可以包含任何数据(eg:jpg图片或者序列化的对象),一个Redis中字符串value最多可以是512M 。
简单动态字符串,是可以修改的字符串,采用分配冗余空间的方式来减少内存的频繁分配 。内部为当前字符串实际分配的空间capacity一般要高于实际字符串长度len。当字符串长度小于1M时,扩容都是加倍现有的空间,如果超过1M,扩容时一次会多扩容1M的空间。
单键多值,双向链表实现。
快速链表quickList
多个zipList使用双向指针串起来使用
set是可以自动排重的,底层实际是一个value为null的hash表,收益添加,删除,查找复杂度都是O(1)
set数据结构是字典,字典是用hash表实现的。内部使用hash结构,所有的value都指向同一个内部值。
Redis hash是一个键值对集合,是一个string类型的field和value的映射表,hash特别适合用于存储对象
Hash类型对应的数据结构是2种:ziplist(压缩列表),hashtable(哈希表)。
当field-value长度较短个数较少时,使用ziplist,否则使用hashtable。
redis有序集合zset是一个没有重复元素的字符串集合。有序集合的每个成员都关联了一个评分(score),这个评分(score)被用来按照从最低分到最高分的方式排序集合中的成员。集合的成员是唯一的,但是评分是可以重复的。
可以实现对位的操作:
是用来做基数统计的算法,HyperLogLog 的优点是:在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。
该类型,就是元素的 2 维坐标,在地图上就是经纬度,redis基于该类型,提供了经纬度设置、查询、范围查询、距离查询,经纬度Hash等常见操作。
消息通信模式:
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.3</version>
</dependency>
package jedis;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPubSub;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author: 308158
* @description: TODO
* @date: Created in 2023-8-17 13:58
*/
public class JedisDemo {
Jedis jedis;
@Before
public void before(){
this.jedis = new Jedis("1.1.1.1", 6379);
}
@After
public void after(){
//关闭jedis
this.jedis.close();
}
/** 测试redis是否连通 */
@Test
public void test1(){
String ping = jedis.ping();
System.out.println(ping);
}
/** string类型测试 */
@Test
public void stringTest(){
jedis.set("site", "http://");
System.out.println(jedis.get("site"));
System.out.println(jedis.ttl("site"));
}
/** list类型测试 */
@Test
public void listTest(){
jedis.rpush("courses", "java", "spring", "springmvc");
List<String> courses = jedis.lrange("courses", 0, -1);
for (String course : courses) {
System.out.println(course);
}
}
/** set类型 */
@Test
public void setTest(){
jedis.sadd("users","tom","jack","ready");
Set<String> users = jedis.smembers("users");
for (String user : users) {
System.out.println(user);
}
}
/** hash类型测试 */
@Test
public void hashTest() {
jedis.hset("user:1001", "id", "1001");
jedis.hset("user:1001", "name", "张三");
jedis.hset("user:1001", "age", "30");
Map<String, String> userMap = jedis.hgetAll("user:1001");
System.out.println(userMap);
}
/** zset类型测试 */
@Test
public void zsetTest() {
jedis.zadd("languages", 100d, "java");
jedis.zadd("languages", 95d, "c");
jedis.zadd("languages", 70d, "php");
Set<String> languages = jedis.zrange("languages", 0, -1);
System.out.println(languages);
}
/** 订阅消息 */
@Test
public void subscribeTest() throws InterruptedException {
//subscribe(消息监听器,频道列表)
jedis.subscribe(new JedisPubSub() {
@Override
public void onMessage(String channel, String message) {
System.out.println(channel + ":" + message);
}
}, "sitemsg");
TimeUnit.HOURS.sleep(1);
}
/** 发布消息*/
@Test
public void publishTest() {
jedis.publish("sitemsg", "hello redis");
}
}
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>1.5.22.RELEASE</version>
</dependency>
#redis服务器ip
spring.redis.host=192.168.1.1
#redis服务器端口
spring.redis.port=6379
#redis密码
spring.redis.password=root
#连接超时时间
spring.redis.timeout=6000
#redis默认情况下有16个分片,默认0
spring.redis.database=0
package jedis;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* @author: 308158
* @description: TODO
* @date: Created in 2023-8-17 14:17
*/
@RestController
@RequestMapping("/redis")
public class RedisController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@RequestMapping("/stringTest")
public String stringTest() {
this.redisTemplate.delete("name");
this.redisTemplate.opsForValue().set("name", "路人");
return this.redisTemplate.opsForValue().get("name");
}
@RequestMapping("/listTest")
public List<String> listTest() {
this.redisTemplate.delete("names");
this.redisTemplate.opsForList().rightPushAll("names", "刘德华", "张学友", "郭富城", "黎明");
return this.redisTemplate.opsForList().range("names", 0, -1);
}
@RequestMapping("setTest")
public Set<String> setTest() {
this.redisTemplate.delete("courses");
this.redisTemplate.opsForSet().add("courses", "java", "spring", "springboot");
return this.redisTemplate.opsForSet().members("courses");
}
@RequestMapping("hashTest")
public Map<Object, Object> hashTest() {
this.redisTemplate.delete("userMap");
Map<String, String> map = new HashMap<>();
map.put("name", "路人");
map.put("age", "30");
this.redisTemplate.opsForHash().putAll("userMap", map);
return this.redisTemplate.opsForHash().entries("userMap");
}
@RequestMapping("zsetTest")
public Set<String> zsetTest() {
this.redisTemplate.delete("languages");
this.redisTemplate.opsForZSet().add("languages", "java", 100d);
this.redisTemplate.opsForZSet().add("languages", "c", 95d);
this.redisTemplate.opsForZSet().add("languages", "php", 70);
return this.redisTemplate.opsForZSet().range("languages", 0, -1);
}
}
}
从输入Multi命令开始,输入的命令都会依次进入命令队列中,但不会执行,直到输入Exec后,redis会将之前的命令依次执行。组队的过程中可以通过discard来放弃组队。
redis事务分2个阶段:组队阶段、执行阶段
事务块内的多条命令会按照先后顺序被放进一个队列当中,最后由 exec 命令原子性(atomic)地执行
假如某个(或某些) key 正处于 watch 命令的监视之下,且事务块中有和这个(或这些) key 相关的命令,那么 exec 命令只在这个(或这些) key 没有被其他命令所改动的情况下执行并生效,否则该事务被打断(abort)。
返回值:
事务块内所有命令的返回值,按命令执行的先后顺序排列。
当操作被打断时,返回空值 nil 。
取消事务,放弃执行事务块内的所有命令。
返回值:总是返回ok
组队中某个命令出现了错误报告,执行时整个队列中所有的命令都会被取消
每次去拿数据的时候都认为会修改,所以每次在拿数据的时候都会上锁,这样别人拿到这个数据就会block直到它拿到锁。传统的关系型数据库里面就用到了很多这种锁机制,比如行锁、表锁、读锁、写锁等,都是在做操作之前先上锁
每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在修改的时候会判断一下在此期间别人有没有去更新这个数据,可以使用版本号等机制。乐观锁适用于多读的应用类型,这样可以提高吞吐量。redis就是使用这种check-and-set机制实现事务的。
在执行multi之前,先执行watch key1 [key2 …],可以监视一个或者多个key,若在事务的exec命令之前这些key对应的值被其他命令所改动了,那么事务中所有命令都将被打断,即事务所有操作将被取消执行 。
如果在执行 WATCH 命令之后, EXEC 命令或 DISCARD 命令先被执行了的话,那么就不需要再执行UNWATCH 了。
因为 EXEC 命令会执行事务,因此 WATCH 命令的效果已经产生了;而 DISCARD 命令在取消事务的同时也会取消所有对 key 的监视,因此这两个命令执行之后,就没有必要执行 UNWATCH 了。
事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断
队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败。
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是键快照文件直接读到内存里。
bgsave命令给到父进程
动态停止RDB:redis-cli config set save “” #save后给空值,表示禁用保存策略
事务中的所有命令都会序列化、按顺序地执行,事务在执行过程中,不会被其他客户端发送来的命令请求所打断
队列中的命令没有提交(exec)之前,都不会实际被执行,因为事务提交前任何指令都不会被实际执行。
事务中如果有一条命令执行失败,后续的命令仍然会被执行,没有回滚。
如果在组队阶段,有1个失败了,后面都不会成功;如果在组队阶段成功了,在执行阶段有那个命令失败就这条失败,其他的命令则正常执行,不保证都成功或都失败。
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的Snapshot快照,它恢复时是键快照文件直接读到内存里。
bgsave命令给到父进程
动态停止RDB:redis-cli config set save “” #save后给空值,表示禁用保存策略
以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令记录下来(读操作不记录),只允追加文件但不可改写文件,redis启动之初会读取该文件重新构造数据,换言之,redis重启的话就根据日志文件的内容将写指令从前到后执行一次以完成数据的恢复工作。
redis.config配置AOF同步的频率
AOF采用文件追加方式,文件会越来越大,为了避免出现此情况,新增了重写机制。
重写机制:当AOF文件的大小超过锁审定的阈值时,Redis就会启动AOF文件的内容压缩,只保留可以恢复数据的最小指令集,可以使用命令bgrewriteaof触发重写
AOF文件持续增长而过大时,会fork出一条新进程来将文件重写(也是先写临时文件,最后在rename替换旧文件),redis4.0版本后的重写,是指就把rdb的快照,以二进制的形式附在新的aof头部,作为已有的历史数据,替换掉原来的流水账操作。
从 Redis 2.4 开始, AOF 重写由 Redis 自行触发, bgrewriteaof 仅仅用于手动触发重写操作。
redis会记录上次重写的aof大小,默认配置是当aof文件大小是上次rewrite后大小的2倍且文件大于64M时触发。
重写虽然可以节约大量磁盘空间,减少恢复时间,但是每次重写还是有一定负担的,因此设置redis满足一定条件才会进行重新。
设置重写的基准值,默认100,当文件达到100%时开始重写(文件是原来重写后文件的2倍时重写)。
设置重写的基准值,默认64MB,AOF文件大小超过这个值开始重写。
主机更新后根据配置和策略,自动同步到备机的master/slave机制,Master以写为主,Slave以读为主。
从机可以采用命令的方式配置
注意:通过 slaveof 命令指定主从的方式,slave重启之后主从配置会失效,所以,重启后需要在slave上重新通过 slaveof 命令进行设置。中途通过 slaveof 变更转向,本地的数据会被清除,会从新的master重新同步数据。
若master下面挂很多slave,master会有压力,实际上slave下面也可以挂slave,配置和上面的类似。
当master挂掉之后,可以从slave中选择一个作为主机。
缺点:需要手动去执行命令去操作,不是太方便。
其他方案:哨兵模式,主挂掉之后,自动从slave中选举一个作为主机,自动实现故障转移。
能够自动监控master是否发生故障,如果故障了会根据投票数从slave中挑选一个作为master,其他的slave会自动转向同步新的master,实现故障自动转义。
需求:配置1主2从3个哨兵
最少有2个哨兵认为主的挂掉了,才进行故障转移。
步骤:
配置
# redis sentinel主服务名称,这个可不是随便写的哦,来源于:sentinel配置文件中sentinel monitor后面跟的那个名称
spring.redis.sentinel.master=mymaster
# sentinel节点列表(host:port),多个之间用逗号隔开
spring.redis.sentinel.nodes=192.168.200.129:26379,192.168.200.129:26380,192.168.200.129:26381
# sentinel密码
#spring.redis.sentinel.password=
# 连接超时时间(毫秒)
spring.redis.timeout=60000
# Redis默认情况下有16个分片,这里配置具体使用的分片,默认是0
spring.redis.database=0
@RequestMapping(value = "/info", produces = MediaType.TEXT_PLAIN_VALUE)
public String info() {
Object obj = this.redisTemplate.execute(new RedisCallback
redis集群是对redis的水平扩容,即启动N个redis节点,将整个数据分布存储在这个N个节点中,每个节点存储总数据的1/N。
例如:由3台master和3台slave组成的redis集群,每台master承接客户端三分之一请求和写入的数据,当master挂掉后,slave会自动替代master,做到高可用。
需求:配置3主3从集群
master1:6379 master2:6380 master3:6381
slave1:6389 slave2:6390 slave3:6391
Redis集群内部划分了16384个slots(插槽),合并的时候,会将每个slots映射到一个master上面,比如上面3个master和slots的关系如下
数据库中的每个key都属于16384个slots中的其中1个,当通过key读写数据的时候,redis需要先根据key计算出key对应的slots,然后根据slots和master的映射关系找到对应的redis节点,key对应的数据就在这个节点上面。
集群中使用公式 CRC16(key)%16384 计算key属于哪个槽
在 redis-cli 每次录入、查询键值,redis都会计算key对应的插槽,如果不是当前redis节点的插槽,redis会报错,并告知应前往的redis实例地址和端口。
连接6379这个实例来操作k1,这个节点发现k1的槽位在6381上面,返回了错误信息。
error:moved slot ip: port
使用redis-cli客户端提供了-c参数可以解决这个问题,表示以集群方式执行,执行命令的时候当前节点处理不了的时候,会自动将请求重定向到目标节点,效果如下,被重定向到6381了
redis-cli -c -h xxxxxx 6379
redirected to slot [12706] located at ip:port(6381)
同样,执行get也会被重定向
如果主节点下线,从节点是否能够提升为主节点?注意:要等15秒
如果某一段插槽的主从都宕机了,redis服务是否还能继续?
要看 cluster-require-full-coverage 参数的值了
# 集群节点(host:port),多个之间用逗号隔开
spring.redis.cluster.nodes=192.168.200.129:6379,192.168.200.129:6380,192.168.200
.129:6381,192.168.200.129:6389,192.168.200.129:6390,192.168.200.129:6391
# 连接超时时间(毫秒)
spring.redis.timeout=60000
当系统中引入redis缓存后,一个请求进来后,会先从redis缓存中查询,缓存有就直接返回,缓存中没有就去db中查询,db中如果有就会将其丢到缓存中,但是有些key对应更多数据在db中并不存在,每次针对此次key的请求从缓存中取不到,请求都会压到db,从而可能压垮db。
比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用大量此类攻击可能压垮数据库
如果一个查询返回的数据为空(不管数据库是否存在),我们仍然把这个结果(null)进行缓存,给其设置一个很短的过期时间,最长不超过五分钟
使用redis中的bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次和bitmap里面的id进行比较,如果访问的id不在bitmaps里面,则进行拦截,不允许访问
实时监控,可以设置黑名单限制对其提供服务
redis中某个热点key(访问量很高的key)过期,此时大量请求同时过来,发现缓存中没有命中,这些请求都打到db上了,导致db压力瞬时大增,可能会打垮db,这种情况成为缓存击穿。
现象:
在redis高峰之前,把热门数据提前存入到redis里面,对缓存中这些热门数据进行监控,实时调整过期时间。
缓存中拿不到数据的时候,此时不是立即去db中查询,而是去获取分布式锁(例如redis中的setnx)拿到锁再去db中load数据,没有拿到锁的线程休眠一段时间再重试整个获取数据的方法。
key对应的数据存在,但是极短时间内有大量的key集中过期,此时若有大量的并发请求过来,发现缓存没有数据,大量的请求就会落到db上去加载数据,会将db击垮,导致服务奔溃。
缓存雪崩与缓存击穿的区别在于:前者是大量的key集中过期,而后者是某个热点key过期。
nginx缓存+redis缓存+其他缓存
用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上,不适用高并发情况
监控缓存,发下缓存快过期了,提前对缓存进行更新。
在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样缓存的过期时间重复率就会降低,就很难引发集体失效的事件。
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题
命令:
set key value NX PX 有效期(毫秒)
表示:
当key不存在的时候,设置其值为value,且同时设置其有效期
eg:
set sku:1:info "ok" NX PX 10000
**表示:**当 sku:1:info 不存在的时候,设置值为ok,且有效期为1万毫秒
执行 set key value NX PX 有效期(毫秒) 命令,返回ok表示执行成功,则获取锁成功,多个客户端并发执行此命令的时候,redis可确保只有一个可以执行成功
客户端获取锁后,由于系统问题,如系统宕机了,会导致锁无法释放,其他客户端就无法或锁了,所以需要给锁指定一个使用期限。
客户端需要实现续命的功能
误删:自己把别人持有的锁给删掉了
例如:
线程A获取锁的时候,设置的有效期是10秒,但是执行业务的时候,A程序突然卡主了超过了10秒,此时这个锁就可能被其他线程拿到,比如被线程B拿到了,然后A从卡顿中恢复了,继续执行业务,业务执行完毕之后,去执行了释放锁的操作,此时A会执行del命令,此时就出现了锁的误删,导致的结果就是把B持有的锁给释放了,然后其他线程又会获取这个锁,挺严重的。
解决:
获取锁的之前,生成一个全局唯一id,将这个id也丢到key对应的value中,释放锁之前,从redis中将这个id拿出来和本地的比较一下,看看是不是自己的id,如果是的再执行del释放锁的操作。
del之前,会先从redis中读取id,然后和本地id对比一下,如果一致,则执行删除,伪代码如下
1.判断redis.get("key").id==本地id是否相当,如果是则执行2
2.del key
此时如果执行step2的时候系统卡主了,比如卡主了10秒,然后redis才收到,这个期间锁可能又被其他线程获取了,此时又发生了误删的操作。
根本原因是:判断和删除这2个步骤对redis来说不是原子操作导致的。
需要使用Lua脚本来解决
将复杂的或者多步的redis操作,写为一个脚本,一次提交给redis执行,减少反复连接redis的次数,提升性能。
Lua脚本类似于redis事务,有一定的原子性,不会被其他命令插队,可以完成一些redis事务的操作。
redis的LUA脚本功能,只能在redis2.6以上版本才能使用
@Autowired
private RedisTemplate<String, String> redisTemplate;
public String lock(){
String lockKey = "k1";
String uuid = UUID.randomUUID().toString();
//获取锁,有效期10秒
if (redisTemplate.opsForValue().setIfAbsent(lockKey, uuid, 10, TimeUnit.SECONDS)){
//业务处理
//使用lua脚本释放锁
String script = "if redis.call('get',KEY[1])==ARGV[1] then return redis.call('del',KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript, Arrays.asList(lockKey), uuid);
System.out.println(result);
return "获取锁成功";
}else{
return "加锁失败";
}
}
为了确保分布式锁可用,至少需要确保分布式锁的实现同时满足以下四个条件