使用
引坐标
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置端口
缓存中存什么
放json字符串,拿出json字符串,还可以逆转为能用的对象类型【序列化与反序列化】
数据库中存什么
当然也存字符串 但是确是可以转换具体类型的字符串 思考一下都什么数据存在Redis里
- 客户端经常访问的热点数据(可能是简单数据)
- 访问一次需要数据库大量操作 比如关联查 或者 查好多个表 返回一个巨大的数据(复杂数据)
举例
现在假设我们从数据库中查询并且组装一个很重的数据结构返回 比如Map<String,List< User>> 这就是getfromDB()的值吧
如此涉及到数据库和Redis中值的转换 每次从DB函数获取的值要JSON.toJSONString(Map<String,List< User>>)
转换为json字符串放到redis里
每次客户端从redis里取值使用 再通过 JSONObject.parseObject(“redisString”,new TypeReference<Map<String,List< User>>>(){})将其强转成客户端操作的数据结构
单个服务器使用
- 缓存的作用就是不走数据库 加快响应速度 所以缓存雪崩+缓存穿透是为防止多走数据库
- 服务在面对多线程请求过来时 要做到资源互斥 这就是缓存雪崩在讨论的
缓存穿透 (空穿)
- 请求访问大量数据库中不存在的数据 当然缓存中也没有 导致每次请求来都去查询数据库 数据库里没值
解决方案:不判断数据库是否有值 只要请求了即使是null 也存入数据库 代码不做任何逻辑判断 直接全部装入redis
缓存击穿 (有击打说明 有热度 )
- 请求访问热点数据 然而热点数据的缓存即将过期 请求还是大量过来 在这一时刻失效了 导致瞬间大量对
一个热点数据的请求
进入数据库 - 进行资源互斥 保证每次只有一个请求进入数据库
缓存雪崩
- redis的k-v对设置的缓存清空时间都差不多 大片K没有对应的value一起失效 全部去请求数据库 导致数据库压力增大 所以在设置缓存清空时间时候设置随机时间
查询数据库并将值放入缓存当中一定是原子性的操作 否则这边查完数据库 刚要放入缓存 其他线程就抢再次去查数据库了
@Test
public Map<String,List<User>> getData(String key) throws JSONException {
String redisJson= (String) redisTemplate.opsForValue().get("key");
if(StringUtils.isBlank(redisJson)){
原子操作 查数据库+放入缓存
gotoDB();
}
return JSON.parseObject(redisJson,new TypeReference<Map<String,List<User>>>(){});
}
private Map<String, List<User>> gotoDB() {
Map<String, List<User>> res=null;
锁住防止缓存击穿 每次只有一个线程进数据库
synchronized (this){
防止第二个获取锁的线程再次查询数据库
if(null!=redisTemplate.opsForValue().get("key")){
return JSONObject.parseObject((String)redisTemplate.opsForValue().get("key"),new TypeReference<Map<String, List<User>>>(){});
}
//模拟...去数据库查询
res=new HashMap();
在原子性里立刻以json形式放到缓存 设置随机失效时间 防止缓存雪崩
redisTemplate.opsForValue().set("key",JSON.toJSONString(res),1,TimeUnit.DAYS);
}
return res;
}
- 从数据库中取值 要加锁防止缓存击穿 同时有大量请求访问数据库 我们加锁保证只有一个请求可以进来访问数据库 其余的在等待队列等待 同时把将数据库值放入缓存也放到锁当中 保证原子性 也保证后续直接从缓存中读 不走数据库了
上面程序为本地加锁 只在当前服务器环境下的访问达到互斥 但是多个不同的服务器即使运行相同的代码因为环境不同 都会访问一次数据库
多台服务器 分布式下使用
-
多台服务器使用就不能用synchronized这种本地锁(只能锁住当前服务器 只能保证当前服务器下的请求只有一个获取锁查询数据库 无法管理其他服务器)
-
我们要自己写一套 和本地锁功能一致的适合 分布式的代码逻辑
核心就是大家共同抢占一个对象 操作一条共享数据 达到互斥
分布式大家都共享同一数据库 数据库的记录可以作为锁
同时redis也可以
set();
get();
setIfAbsent();
是不同的方法 前两个是缓存的功能存储拿出
后面是抢占锁相关的
分布式下只保证一个请求进入数据库 锁就变成根据redis来获取锁了
简单来说就是我们自己
使用redistemplate来编写一个类似synchronized功能的分布式锁
redistemplate.setIfAbsent()
返回一个布尔值 说明当前线程是否抢到了锁
如果抢到锁了 就可以进行
抢锁
- synchronized是自己内部维护 自己
到时间销毁锁
我们使用redistemplate抢占锁也要记得写上过期时间
防止一直占锁 - 本地锁多线程抢锁维护一个对象 达到互斥
分布式抢锁我们自己写uuid作为当前占据锁(key)的 钥匙值(value)保证了互斥 独占 原子性
抢锁失败等待
- 本地锁对象有一个等待队列 线程在自旋锁等待抢占锁
我们自己实现也要达到这个效果,如果布尔值
没抢到锁 就休眠一会然后再次调用访问当前函数 去抢占
释放锁
- 在抢锁时为了保证独占性 互斥性 我们在抢到的锁上加了自己的钥匙值 现在要将这个锁释放 要删除我们独占的钥匙
- 删除独占钥匙的第二个情况 k-uuid1设置的清空缓存时间是10s 业务太长走了20s
此时锁早已经被别人抢走了 k-uuid2了 所以删除锁要对应自己的名字 否则把别人正在走业务的线程释放锁 就报错了 - uuid的值是赋值操作 在多线程里赋值操作是最无法保证原子性的 当uuid1走到了删除锁 想通过uuid1.equals()来释放锁的时候发现uuid1已经被赋值改为uuid2了 我们只能用lua脚本来释放锁
原子性+过期解锁+自己的锁名字(防止将别人的锁释放)
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
1、占分布式锁。去redis占坑
设置过期时间必须和加锁是同步的,保证原子性(避免死锁)
String uuid = UUID.randomUUID().toString(); 无法保证原子性 会被篡改
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if (lock) {
System.out.println("获取分布式锁成功...");
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
加锁成功...执行业务 这里去访问数据库就不用sychronized本地锁了
dataFromDb = getDataFromDb();
} finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
删除锁
stringRedisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
//先去redis查询下保证当前的锁是自己的
//获取值对比,对比成功删除无法保证uuid没被修改值原子性 lua脚本解锁
// String lockValue = stringRedisTemplate.opsForValue().get("lock");
// if (uuid.equals(lockValue)) {
// //删除我自己的锁
// stringRedisTemplate.delete("lock");
// }
return dataFromDb;
} else {
System.out.println("获取分布式锁失败...等待重试...");
//加锁失败...重试机制
//休眠一百毫秒
try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); }
return getCatalogJsonFromDbWithRedisLock(); //自旋的方式
}
}
private Map<String, List<Catelog2Vo>> getDataFromDb() {
//得到锁以后,我们应该再去缓存中确定一次,如果没有才需要继续查询
String catalogJson = stringRedisTemplate.opsForValue().get("catalogJson");
if (!StringUtils.isEmpty(catalogJson)) {
//缓存不为空直接返回
Map<String, List<Catelog2Vo>> result = JSON.parseObject(catalogJson, new TypeReference<Map<String, List<Catelog2Vo>>>() {
});
return result;
}
System.out.println("查询了数据库");
//3、将查到的数据放入缓存,将对象转为json
String valueJson = JSON.toJSONString(parentCid);
stringRedisTemplate.opsForValue().set("catalogJson", valueJson, 1, TimeUnit.DAYS);
return parentCid;
public Object getData(){
抢锁
if(block){
try{
查缓存
查数据库+存如缓存
}finally{
释放锁
}
}else{
自旋锁
}
}
Redisson
使用分布式锁刚才是我们自己用redisTemplate代码实现的
不仅承担了存储 还承担了分布式锁的功能
- 也在上面讲了就是在模拟单机的synchronized的功能
比如过期自动释放锁 抢锁失败自旋锁等待 删除锁等
现在我们使用redission框架成熟完成以上功能 而不是自己实现
引入坐标
配置redis地址及端口
- 文件
- 配置类
使用我们自己创建@Bean Redissionclient
抢锁 释放锁
try{
redissionCLient.lock();
}catch{
}finally{
redissionClinet.unlock();
}
看门狗原理
redistemplate 里面有原子的 抢锁+过期时间 确保不会因为 服务器突然宕机 等突发情况 没有为锁设置过期时间而导致一直占着锁 成为活的死锁
但redission的lock没有显式的让我们看到他有设置原子性的过期时间