
上一篇:Redis 系列04--Spring Data Redis_fengxianaa的博客-CSDN博客
1. 代码准备增加依赖
org.springframework.boot
spring-boot-starter-jdbc
mysql
mysql-connector-java
5.1.46
修改 application.properties ,增加数据库配置
#数据库
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/数据库名
spring.datasource.username=
spring.datasource.password=
数据库中有一条数据
修改 User 类
public class User {
public int id;
public String name;
public int age;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
新增 UserService 类
@Service
public class UserService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
public User getUser(int id){
System.out.println("先从缓存中获取......");
String s = stringRedisTemplate.opsForValue().get(id+"");
if(StringUtils.hasText(s)){
return JSONObject.parseObject(s, User.class);
}
System.out.println("缓存中没有再查询数据库......");
User user = jdbcTemplate.queryForObject("select * from user where id= ?",
new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
System.out.println("放到缓存中......");
//注意设置缓存的时候,一定要设置超市时间
stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(user), 30, TimeUnit.SECONDS);
return user;
}
}
新建 UserController
@RestController
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/get")
public Object getUser(@RequestParam int id){
return userService.getUser(id);
}
}
浏览器访问结果
后台输出:
再次访问,就不会查询数据库了
2. 数据库、redis数据不一致问题有了缓存,查询数据确实方便,但是修改数据,除了更新数据库,也要更新 redis 缓存
一般采用两种方案:
- 修改数据后,在把数据放到缓存中
- 直接删除缓存
第一种需要 先查询-->修改-->更新缓存,比较麻烦,而且如果这个数据不经常使用,平白占用内存
所以一般都是使用第二种,在修改数据的时候,直接删除缓存
无论哪种方案,都逃不过:数据库、redis数据不一致问题
- 先更新数据库,再删缓存
如果代码按照上图中的 1、2、3、4 执行,就会造成数据库是新数据,redis 是老数据
2. 先删缓存,再更新数据库
如果代码按照上图中的 1、2、3、4 执行,还是会造成数据库是新数据,redis 是老数据
以上两种方案都无法解决数据的高一致性
解决方案延时双删:修改数据的线程,删除缓存-->更新数据库-->延时20毫秒再删缓存
这样就能保证数据库和redis的实时一致性
如果不睡眠20毫秒,可能会产生以下情况
如果你的代码效率高,机器性能足够好,就可能产生上图的情况,第三步3和第4步瞬间完成,
之所以 睡眠20毫秒,就是为了,让 “再次删除缓存” 这个 *** 作在最后执行
当然这个20毫秒不是固定值,睡眠 30、40、50 都可以
代码public void updateAge(int id, int age){
//删除缓存
stringRedisTemplate.delete(id+"");
//更新数据库
jdbcTemplate.update("update user set age=? where id=? ", age, id);
try {
Thread.sleep(20);
} catch (InterruptedException e) {
}
//再删除缓存
stringRedisTemplate.delete(id+"");
}
其他方案
保证对某条数据的查询、修改 *** 作同步执行
对数据id进行路由,把同一条数据的 *** 作都放到同一个队列中,用其他线程一个个处理
3. 缓存穿透缓存穿透:用户查询的数据在 缓存 和 数据库中都不存在,对这些数据的请求会直达数据库,给数据库造成很大的压力,若黑客利用此漏洞进行攻击可能压垮数据库。
解决方案- 布隆过滤器,
- 原理:对已有数据的key进行hash,然后放到布隆过滤器中,在请求到达 缓存 和 数据库之前,先用布隆过滤器判断这个 key 是否存在,不存在就直接返回
- 举一个极端的例子,假设你生成的数据 id 从 1 开始且有序,
- 从内存中划出一块空间,设置其中的每一位,(8位 = 1byte,1024byte = 1kb)
id是 1 的数据存在,就设置第 1 位=1
id是 2 的数据不存在,就设置第 2 位=0,依次类推
这样当请求过来时,判断对应 位 上的数据是否为 1,来进行过滤
- 从内存中划出一块空间,设置其中的每一位,(8位 = 1byte,1024byte = 1kb)
- redis 的 bitmap 可以做这个事情(之后再说)
- 原理:对已有数据的key进行hash,然后放到布隆过滤器中,在请求到达 缓存 和 数据库之前,先用布隆过滤器判断这个 key 是否存在,不存在就直接返回
- 设置空对象
- 如果数据库查不到这条数据,那么就在 缓存中设置一个NULL
- 问题:
- 设置NULL,虽然有过期时间,但会占用一些空间
- 可能造成缓存和数据库的数据短期内不一致,假如过期时间30秒,设置id=100的为null,此时其他线程增加了这个数据,那么30秒内 缓存和数据库 不一致
这时可以在新增数据成功后尝试删除缓存
代码:
public User getUser(int id){
System.out.println("先从缓存中获取......");
String s = stringRedisTemplate.opsForValue().get(id+"");
if(StringUtils.hasText(s)){
//如果从redis中查到null,返回:没有这个用户
if ("null".equals(s)) {
User user = new User();
user.setName("没有这个用户");
return user;
}
return JSONObject.parseObject(s, User.class);
}
System.out.println("缓存中没有再查询数据库......");
//这里跟之前不一样,换成了query方法
List users = jdbcTemplate.query("select * from user where id= ?",
new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
if(CollectionUtils.isEmpty(users)){
//没有从数据库中查到数据,就设置一个null
stringRedisTemplate.opsForValue().set(id + "", "null", 30, TimeUnit.SECONDS);
User user = new User();
user.setName("没有这个用户");
return user;
}else{
System.out.println("放到缓存中......");
stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)), 30, TimeUnit.SECONDS);
}
return users.get(0);
}
结果:
4. 缓存击穿其实就是热点数据问题,如果一个 key 是热点数据,就在它刚刚过期时,这时候大量请求来访问这个 key,因为没有缓存,所以这些请求都直达数据库。
解决方案- 加锁
- 上图,同时两个线程访问一个key,而且都未命中缓存,所以开始加分布式锁
- 线程 1 加锁成功,就从数据库中获取数据,然后写入缓存,最后释放锁
- 线程 2 加锁失败,就休眠一定时间,然后查询缓存,查不到就重新申请加锁
- 缺点:线程等待,影响一些性能
代码:
public User getUser(int id){
String s = stringRedisTemplate.opsForValue().get(id+"");
if(StringUtils.hasText(s)){
return JSONObject.parseObject(s, User.class);
}
//1. 未命中,加分布式锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(id + "_lock", "lock", 60, TimeUnit.SECONDS);
if(lock){
//2. 加锁成功,查询数据库,并写入缓存
List users = jdbcTemplate.query("select * from user where id= ?",
new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)), 30, TimeUnit.SECONDS);
//3. 释放锁,解锁的代码要写到finally中
stringRedisTemplate.delete(id + "_lock");
return users.get(0);
}else{
//4. 加锁失败,休眠20毫秒,继续查询缓存
try {
Thread.sleep(20);
} catch (InterruptedException e) {}
return getUser(id);
}
}
- 逻辑过期
- 最大特点是,设置缓存时,不设置过期时间,而是在缓存中加一个字段(expiretime),记录这条数据的过期时间,比如:
- 每次从缓存中拿出时,如果 当前时间 > expire,那么就重新查询数据库,更新缓存
- 缺点:实现相对复杂,无法保证数据是最新的
- 最大特点是,设置缓存时,不设置过期时间,而是在缓存中加一个字段(expiretime),记录这条数据的过期时间,比如:
代码:
public User getUser(int id){
//1. 从缓存获取数据
String s = stringRedisTemplate.opsForValue().get(id+"");
User user = null;
if(StringUtils.hasText(s)){
user = JSONObject.parseObject(s, User.class);
}
if(user == null || System.currentTimeMillis() >= user.getExpire()){
//2. 如果已经过期,加分布式锁
Boolean lock = stringRedisTemplate.opsForValue().setIfAbsent(id + "_lock", "lock", 60, TimeUnit.SECONDS);
if(lock){
//3. 新建线程,查询数据库,并写入缓存
new Thread(() -> {
List users = jdbcTemplate.query("select * from user where id= ?",
new Object[]{id}, new BeanPropertyRowMapper<>(User.class));
users.get(0).setExpire(System.currentTimeMillis() + 60000);
//注意:不设置过期时间
stringRedisTemplate.opsForValue().set(id + "", JSONObject.toJSONString(users.get(0)));
//4. 释放锁,解锁的代码要写到finally中
stringRedisTemplate.delete(id + "_lock");
}).start();
}
}
return user;
}
5. 缓存崩溃
也叫缓存雪崩,由于 redis 宕机 或者 同时间大量的key过期,导致大量请求到达后端数据库,从而导致数据库崩溃,整个系统崩溃
解决方案- 事前
- redis集群
- 多机房部署,避免一起宕机
- 多套redis集群,之间做数据同步
- 事中
- 对源服务访问限流
- 对redis服务访问限流
- 多级缓存
- 事后
- redis 利用备份数据做数据恢复
欢迎分享,转载请注明来源:内存溢出
微信扫一扫
支付宝扫一扫
评论列表(0条)