1.缓存使用
本地缓存
本地缓存在分布式项目下的问题(各个服务拥有各个服务的缓存组件)
同一个用户在访问时可能会访问不同的服务,这样就会导致之前在别的服务上的缓存无法使用,也会产生数据一致性的问题
分布式缓存
2.分布式应用redis的使用
引入依赖
简单配置reids地址等信息
使用spring提供的StringRedisTemplate来操作redis
例子:保存字符串并查询
实际应用(简单使用)
压力测试下出现的问题-堆外内存溢出
问题解决:切换使用jedis
1.引入依赖
3.缓存穿透,雪崩,击穿
穿透:是指高并发环境下,多个请求同时查询一个一定不存在的数据,由于缓存不命中,将查询数据库,但数据库中也没有这条记录,这将导致这个不存在的数据每次请求都要到数据库去查,失去了缓存的意义。
解决:null结果缓存,并加入短暂获取时间,springcache提供了一个配置ache-null-value将此配置开启即可缓存空数据,也可以使用布隆过滤器
雪崩:是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一刻同时失效,请求全部访问数据库导致数据库压力过重雪崩
解决:分析用户的行为,尽量让缓存失效的时间均匀分布。如果是因为某台缓存服务器宕机,可以考虑做主备,比如:redis主备
击穿:对于一些设置了过期时间的key,如果这个key是一个超高并发访问的热点数据,且这个key在大量请求进入后刚好失效,那么所有的请求都将落到数据库。
解决:加锁大量并发只让一个人去查,其他人等待,查到以后释放锁,其他人获取锁先查缓存就会有数据。
springcache提供了@cacheable(sync=true)(加锁解决击穿)/或者自己加锁应注意锁的添加(setnx)和删除(lua脚本)都应是原子性的
4.加锁方式解决缓存穿透
4.1本地锁
单体如何应用加锁?
加锁应注意锁的时序问题(数据库查到结果后立即将结果放到缓存中且在将数据放在redis中之后再释放锁,查出数据后需要将数据放到redis这是一个短暂的过程,但若在这个过程中恰好有一个请求进入且锁释放就会出现2次查询数据库的现象)
加锁后应再次判断缓存数据是否存在有就直接返回。(因为以有所有的线程都已经执行了外层的判断且在synchronized处等待,若不加内层判断当第一个拿到锁的请求执行完后之后所有的线程都会依次的查询数据库)
本地锁存在的问题
本地所只能锁住当前服务,而分布式下会有应用集群,每把锁只能锁住当前应用就会出先多个请求同时访问数据库。
4.2分布式下如何加锁
使用redisTemplate提供的setIfAbsent(“lock”,“任意字符串”)方法占位;方法意为,若多个请求同时访问只有一个能将数据放入lock字段且返回true,其他返回false
返回为true的进行逻辑代码到数据库查询相关数据并放入缓存,且将之前lock数据删除供其他服务获取;
返回为false的再去重新占位获取锁;(synchronized锁是自旋锁当代码执行到大括号结尾时会自动释放)应注意对内存溢出异常此处建议加上休眠时间。
问题:若在某个请求获取到锁后出现异常或者断电就会出现占位数据lock无法被删除,导致所有请求全部在等待锁的问题即死锁问题
注占位和设置过期时间必须要是原子的,要么都成功要么都失败
**问题:**如果在我们执行业务代码时,我们前面的占位lock数据已经过期,这样的话,我们在执行到删除占位lock数据时,会删除一个不存在的数据,更严重的话若时第二个请求刚好占到位了就会出现将我们第二占位lock数据删除,但三个请求也是如此.
若业务超时也会造成安全隐患。
**问题:**因为在我们从redis中查出自己的占位数据是需要短暂的时间 ,若在这短暂的时间例占位数据过期,且我们刚好完成了if(uuid.equals(lockValue))的判断,这时再有新的请求进入,那么就会重新添加占位数据,我们再执行删除占位数据,就会删除新的请求的占位数据,之后新的请求在进行判断时就会出现死锁问题,
所以删除占位数据与查询自己的占位符操作也应该是原子的
总结 :分布式锁应该原子加锁(setnx)解锁(Lua脚本解锁)。
5.分布式锁-Redisson
分布式环境下同一台服务会搭建集群出现同一份代码部署在不同的机器上
- 使用显示锁和隐式锁只能锁住本地的线程,因为这两个锁的都是针对jvm下的锁对象进行操作,在不同的jvm下失效
- 虽说使用原子加锁(使用setnx命令创建键值并指定过期时间),解锁(删除键值)的方式可以完成分布式锁的功能,但时存在一定的隐患,若设置的过期时间为10s而这时线程1执行的业务代码过慢10s之内没执行完这时会直接将加的锁删除,而线程2发现锁已经被删除,就重新上一把锁会直接进入代码,与此同时线程1业务执行完了会将锁删除,这就导致了线程1删除了线程2锁的问题
- 2中的问题可以通过在finally块中删除锁的时候加一个判断不同的锁使用不同的对象可以使用获取随机数的方式拼接字符串加锁
- 虽然3中解决了删除了别人锁的问题,但是3中的删除锁的操作不是原子的了,所以我们不用这种方式解决2中存在的问题,对于3中的问题可以使用lua脚本进行原子的删除键值操作(使用jedis执行lua脚本),还可以使用redis事务的命令来解决,使用watch,multi ,判断是否为当前锁并删除锁 ,exec,unwatch
- 上述能实现分布式锁,但我们写的代码性能较低,所以可以使用redisson,redisson提供了类似于juc包下的Lock接口的使用方式使用lock,unlock实现分布式锁可能出现
可以使用unlock时要注意加判断
redisson官方文档
1.导入依赖
<!-- https://mvnrepository.com/artifact/org.redisson/redisson-spring-boot-starter -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-spring-boot-starter</artifactId>
<version>3.12.0</version>
</dependency>
2.程序化配置测试redisson
所有的redisson操作都是用redissonClient
@Configuration
public class RedissonConfig {
@Bean(destroyMethod="shutdown")
RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.1.5:6379");
RedissonClient redisson = Redisson.create(config);
return redisson;
}
}
测试代码
@Autowired
RedisClient redisClient;
@Test
void contextLoads1() {
System.out.println(redisClient);
}
5.1 lock锁测试
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。
redisson的锁底层使用juc实现
lock.lock();:有自动续期功能;
lock.lock(10 , TimeUnit.Seconds);设置自动解锁时间自动续期失效导致问题
看门狗自动续期和自动解锁原理解析
在给锁添加自动过期时间之后,自动续期功能失效,导致解锁失败,
每隔十秒钟都会自动再次续期,续成30s
5.2 读写锁的使用测试
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口
模仿读写锁业务
5.3闭锁测试(等待)
基于Redisson的Redisson分布式闭锁(CountDownLatch)Java对象RCountDownLatch采用了与java.util.concurrent.CountDownLatch相似的接口和用法。
模拟:放假学校锁门状态,要学校5个班里所有的人全部走完才能锁门;
5.4信号量测试(用可用于分布式的限制流量)
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。
模拟车库停车,总共三个车位给redis中添加一个key =“park” value="3"停车就会减一个车位,开走就会多一个车位,没车位时会线程阻塞等待。
6.缓存一致性解决
模拟业务,若我们缓存品牌信息后,又修改了商品信息,这时再去查询品牌就会出现缓存与数据库数据不一致问题,提供两种解决方案
改数据库同时写入缓存(双写模式)最终一致性前提是,写入缓存时必须设置缓存过期时间。
两个请求同时访问:写数据库1操作完成后用户网络卡顿,此时写数据库2与写缓存2都完成后,才执行写缓存1。就出现了数据的不一种
修改数据库时,将缓存设置为失效下次查询自动更新(失效模式)
canal
我们系统的一致性解决方案:
1.缓存的所有数据都有过期时间,数据过期下一次查询就能主动更新
2.读写数据时加上分布式读写锁
7.整合redis缓存的使用(这里只简单过一下详细可参考spring缓存抽象、整合redis
1.依赖导入
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.配置
springcache 给我们的写好了的自动配置
CacheAutoConfiguration会导入RedisAtuoConfiguration;RedisAtuoConfiguration中自动配置好了CacheManager组件
我们只需要配置缓存的类型为redis
spring.cache.type=redis
其他配置:
3.使用@cacheable后的默认行为与自定义
默认行为:
- 如果缓存中有方法不再调用
- key默认自动生成缓存的名字为SimpleKey
- 缓存的value的值,默认使用java序列化机制,将序列化后的数据存到redis
- 默认失效时间为-1(永不失效)
自定义我们自己的行为 - 自定义缓存的名字(cacheName/value="#root.arg[0] "可使用#p0/#a0#root.arg[0]获取方法中参数的值作为缓存名#root也有很多内置对象如method,target等)
- 自定义过期时间(在配置文件中spring.cache.redis.tine-to-live=6000设置缓存过期时间,单位为毫秒)
- 自定义序列化机制(默认为jdk的序列化格式)
修改为自定义序列化配置(参考spring缓存抽象、整合redis)
分区名时spring中的一个概念就是cacheable(value=“product” key=“product1”)value的值就是分区名,key的值才是真正的缓存名
总结:分布式中的缓存一致性解决方案,
双写模式使用@cacheput(value=“product”,key="#product.id")
失效模式使用@cacheEvict(value=“product”,allEntries=true)
注:@caching可以实现多个@cacheput 或@cacheEvict操作
8.redis持久化如何实现
- RDB:对数据执行周期性的持久化。—若服务器宕机会丢失当前一次周期的数据 ,数据恢复性能高,但可能会数据丢失
原理:创建一个单独的子线程,先将数据写入一个临时文件中,等到上一个文件持久化完成后用这个临时文件替换上次持久化好的文件。
配置:redis.conf文件,去配置持久化 rdbSave(生成RDB文件)和rdbLoad(从文件加载内存)
save 60 1000 //每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件 - AOF:机制对每条写入命令作为日志记录,以append-only的模式写入一个日志文件中。重启时通过日志文件重新写入数据,数据回复时慢,安全性高
原理:定时任务一个后台线程执行一次同步到硬盘上的操作时 flushAppendOnlyFile函数会被调用
配置:redis.conf文件,去配置持久化 ,appendonly yes //可以打开AOF持久化机制,一般来说AOF都是要打开的。
在使用时两种全部开启,重启时会将RDB放到AOF的头部。
9.redis不同的数据类型的使用场景
redis 命令是不区分大小写的,而键值是区分大小写的
9.1redis传统五大数据类型的应用
9.1.1String 类型的应用
- 常用命令
set key value 创建一个键值
get key 获取一个键的值
mset k1 v1 k2 v2 k.v3 创建多个键值
mget k1 k2 k3 获取多个键值
append key value :给key拼接字符串
del key :删除一个键值 - 数值增减
incr 例:incr s 每使用一次s键对应的值加一默认从0开始
incr key 整数值 例incr s 3 每使用一次s键对用的值加3默认从零开始
decr 例:decr s 每使用一次s键对应的值减一默认从0开始
decr key 整数值 例:decr s 3 每使用一次s键对用的值减3默认从零开始
获取指定键的长度是多少;strlen key
当key不存在,则创建键值;setnx key value
set key value [EX seconds] [PX millseconds] [NX|XX]
EX:key在多少秒之后过期;PX:key在多少毫秒之后过期;NX:当key不存在才创建key;XX:当key存在时覆盖key; - 应用场景
商品编号、订单编号使用incr命令生成
文章喜欢的人数:使用incr命令点击一次增加一个
9.1.2hash类型的应用
redis的hash结构对应JAVA中的Map<String,Map<Object,Object>>
- 常用命令
给key设置一个字段值;hset key field value 例:hset person id 1 给person这个键设置id字段值为1。
获取一个key的一个指定字段值;hget key field value 例:hget person id 获取person这个键的id字段值。
给key设置多个字段值;hmset key filed1 value1 filed2 value2
例:hmset person age 20 weight 100给person这个键设置age字段值为20,体重字段值为100。
获取key的多个字段值;hmget key filed1 filed2 例hmget person age weight 获取key的年龄和体重字段值;
获取key的所有字段值;hgetall key
获取某个key内的全部数量; hken key
删除一个key;hdel key - 应用场景购物车
用户1024新增一个11111商品数量为1–>hset shopcar:uid1024 11111 1
用户1024新增一个22222商品数量为1–>hset shopcar:uid1024 22222 1
为用户1024的22222商品数量+1–>hincrby shopcar:uid1024 22222 1
购物车显示总数–>hlen shopcar:uid1024
全部选择:hgetall shopcar:uid1024
9.1.3list类型的应用
List类型是按照插入顺序排序的字符串链表
- 常用命令
向列表左边添加元素lpush key values[value1 value2 …]例 lpush source 30 56 78
向列表右边添加元素rpush key values[value1 value2 …]例 lpush source 80 92 99
弹出列表左边元素;若key不存在则返回nil; lpop key
弹出列表右边元素;rpop key:
获取链表中从start到end的元素的值; lrange key start end;start、end从0开始计数;若为-1则表示链表尾部的元素,-2表示倒数第二个
查看列表所有元素;lrange key start stop;
返回指定key关联的链表的元素个数 llen key - 微信订阅公众号
假设现在关注了两个公众号,新发布的文章id分别为11和12,通过lpush likearticle:userid 11 12将新发布的文章安装进我在redis中关注的list
查看订阅公众号的全部文章前十条 lrange likeartcle:userid 0 10
9.1.4set类型的应用
类似与java中的hashset无序无重复
- 常用命令
在指定key对应的集合中添加 value集合,如果key不存在,即新创建; sadd key value1 value2 value3
移除指定key中的value1,value2集合,并返回移除的值的数量;srem key value1 value2
判断元素是否在集合中,返回 0 或 1;sismember key value
返回指定key对应的集合中的所有元素; smembers key
返回指定key对于应集合中元素个数;scard key
从集合中随机弹出n个元素 元素不删除;srandmember key n
从集合中随机弹出n个元素,元素删除;spop key n
集合的差集运算 sdiff key1 key2;获取属于key1集合但不属于key2集合的元素
集合的交集运算 sinter key1 key2;获取即属于key1集合,也属于key2集合的元素
集合的并集运算 sunion key1 key2;获取key1,key2集合中的所有元素
应用场景:
- 微信用户抽奖;
用户点击立即参与抽奖按钮 sadd key uid;将用户id放入set集合中
显示有多少人参加了 scard key;显示set总数
抽奖从set中任意抽取n人并移除 spop key n - 微信朋友圈点赞
现在发布了一条微信朋友圈msgid = 111
每有人点赞都会新增点赞 sadd pub:msgid 点赞用户id1 点赞用户id2
每有有人取消点赞都会srem pub:msgId 点赞用户
展现所有点赞的用户 smembers pub:msgId
点赞总数显示 scard pub:msgId
判断某个朋友是否对楼主点赞过 sismember pub:msgId 用户id - 微博好友关注社交关系
共同关注的人:进入好友微博立即显示共同关注的人:将两个人的关注集合取交集sinter user1关注集合 user2关注集合
我关注的人也关注了他:取初当前用户的关注集合,和我关注的人做交集 - QQ内推可能认识的人
取当前用户与任意用户的好友的差集
9.1.5Zset类型的应用
向有序的集合中加入一个元素和该元素的分数
- 常用命令
添加一个键值并指定分数;zadd key score value 例zadd movie 20 tom 添加一个key为电影,值为tom评分为20
给指定键的的指定值添加分数;zincrby key increment value
从小到大返回start 到 stop之间的元素,withscores是否显示分数;zrange key start stop [withscores显示就加上不显示就去除]
从大到小返回start 到 stop之间的元素zrange key start stop [withscores]
从小到大返回指定value的下标;zrange key value
从大到小返回指定value的下标;zrange key value
通过下标删值;zremrangebyrank key start stop
通过分数范围删值;zremrangebyscore key min max
获取集合中元素数量;zcard key
获取分数在指定范围的元素,limit用作分页;zrangebyscore key min max [withscores] [LIMIT offset count]
使用场景:
- 根据商品销售对商品进行排序显示
定义商品销售排行榜sellsort集合,key为goods:sellsort 值为各个商品的id,分数为各个商品的销量
商品id为3和5的卖出各一件:zadd goods:sellsort 1 3 1 5
商品id为3的又卖出了10件:zincrby goods:sellsort 10 3
展示产品销量的前十名:zrange goods:sellsort 0 10 withsorces
10.redis架构————》主从复制
- 是什么,能干嘛?
主机数据更新后根据配置和策略,自动同步到备机的master/slaver机制,Master以写为主,Slave以读为主读写分离,容灾恢复 - 如何使用?
配从(库)不配主(库)
从库配置:slaveof 主库IP 主库端口
修改配置文件细节操作 - 常用的三种架构:
一主二仆 --> 一个Master两个Slave
薪火相传 --> 上一个Slave可以是下一个slave的Master,Slave同样可以接收其他slaves的连接和同步请求,那么该slave作为了链条中下一个的master,可以有效减轻master的写压力
反客为主–> 使当前数据库停止与其他数据库的同步,转成主数据库
哨兵模式–> 反客为主的自动版,能够后台监控主机是否故障,如果故障了根据投票数自动将从库转换为主库 - 复制的缺点
由于所有的写操作都是先在Master上操作,然后同步更新到Slave上,所以从Master同步到Slave机器有一定的延迟,当系统很繁忙的时候,延迟问题会更加严重,Slave机器数量的增加也会使这个问题更加严重。
11. redis的事务
Redis事务特性
事务中所有的命令都会被串行化执行,事务执行期间Redis不会为其他客户端的请求提供任何服务,保证事务中所有命令被原子地执行。
在Redis事务中如果有一条命令执行失败,其后的命令仍然会被继续执行,不提供回滚操作;
Redis事务命令:
- MULTI-事务开始, 该命令返回OK提示信息. Redis不支持事务嵌套,执行多次MULTI命令和执行一次是相同的效果.嵌套执行MULTI命令时,Redis只是返回错误提示信息.
- EXEC-事务提交,事务中的命令序列将被执行(或者不被执行,比如乐观锁失败等).该命令将返回响应数组,其内容对应事务中的命令执行结果.
- WATCH-开始执行乐观锁,该命令的参数是key(可以有多个), Redis将执行WATCH命令的客户端对象和key进行关联,如果其他客户端修改了这些key,则执行WATCH命令的客户端将被设置乐观锁失败的标志.该命令必须在事务开始前执行,即在执行MULTI命令前执行WATCH命令,否则执行无效,并返回错误提示信息.
- UNWATCH-取消当前客户端对象的乐观锁key,该客户端对象的事务提交将变成无条件执行.
- DISCARD-结束事务,并且会丢弃全部的命令序列.需要注意的是,EXEC命令和DISCARD命令结束事务时,会调用UNWATCH命令,取消该客户端对象上所有的乐观锁key。