redis中某一个热点key过期,大量的请求压到数据库上,可以使用分布式锁来解决 redis提供的分布式锁功能
用jedis进行举例,对应的方法为
public Long setnx(String key, String value);//当key不存在时设置,成功返回1(相当于获取锁),失败0 public Long expire(String key, long seconds);//设置key的过期时间 public Long del(String key);//删除key,相当于释放锁
解决缓存击穿的思路:
1.线程请求redis,如果有数据直接返回。
2.redis中没有数据,通过查询的key和表名拼接一个新的key作为锁,存储到redis中(setnx成功返回1,失败返回0)。
3.获取到锁的线程(返回值为1)放行到数据库查询,并且设置一个锁的过期时间(expire),其他线程继续请求redis(轮询一定次数抛异常)。
4.获取到锁的线程从数据库的查询的结果写入redis,并且释放锁(del)。
用一个类来模拟数据库:
class Database {
//模拟数据被请求的次数
public static int count = 0;
}
定义一些前置信息:
//redis连接信息 private static final String REDIS_HOST = "192.168.1.9"; private static final Integer REDIS_PORT = 6379; //模拟热点数据的key private static final String USER_ID = "40d665c8-7652-468e-8f0f-95666cdc304c"; //数据库表名称 private static final String TABLE_NAME = "tb_user";
定义模拟查询业务的方法:
//模拟查询业务
private void selectData() {
Jedis jedis = getJedis();//从jedisPool中获取jedis对象
//从redis中查询数据
String result = jedis.get(USER_ID);
if (!(result == null || "".equals(result.trim()))) {//缓存有数据,直接返回
System.out.println("直接从缓存中查询出user数据:" + result);
return;
}
//缓存中没有数据 从数据库查
//分布式锁的key设置为 ‘表名.key’
String lockKey = TABLE_NAME + "@" + USER_ID;
//加锁
Long lock = jedis.setnx(lockKey, "lock");
if (lock == 1) {//获取锁成功,设置锁超时30s
jedis.expire(lockKey, 30L);
System.out.println("获取到了锁...");
//从数据库中查询数据
Database.count++;
//数据库的数据设置到redis中
jedis.setex(USER_ID,300L, "从数据库中查询到的user对象" + Database.count);
System.out.println("数据缓存完毕...");
//添加完成,释放锁
jedis.del(lockKey);
System.out.println("释放了锁...");
return;
}
//未成功获取到锁,重试查询redis 5次
int count = 5;
while (count > 0) {
try {
TimeUnit.MILLISECONDS.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
String value = jedis.get(USER_ID);
if (value == null || "".equals(value.trim())) {
count--;
continue;
}
//如果查询到了数据 return
System.out.println(Thread.currentThread().getName() + "从redis中获取到了数据==>" + value);
return;
}
//重试5次仍然没有从redis中查询到数据
throw new RuntimeException("重试5次没有查询到数据,抛出到异常处理器进行处理");
}
开启100个线程测试:
@Test
//模拟多线程查询
public void concurrentSelect() {
for (int i = 0; i < 100; i++) {
new Thread(this::selectData).start();
}
while (true){}//主线程不能结束
}
控制台输出的结果如下:
获取到了锁... 数据缓存完毕... 直接从缓存中查询出user数据:从数据库中查询到的user对象1 直接从缓存中查询出user数据:从数据库中查询到的user对象1 释放了锁... 直接从缓存中查询出user数据:从数据库中查询到的user对象1 直接从缓存中查询出user数据:从数据库中查询到的user对象1 ... Thread-57从redis中获取到了数据==>从数据库中查询到的user对象1 Thread-94从redis中获取到了数据==>从数据库中查询到的user对象1 Thread-46从redis中获取到了数据==>从数据库中查询到的user对象1 Thread-81从redis中获取到了数据==>从数据库中查询到的user对象1 Thread-71从redis中获取到了数据==>从数据库中查询到的user对象1
从控制台的结果可以看出,无论多少个线程请求该热点数据的key,也只有一个会压到数据库中,避免了缓存击穿的问题。设置到redis中key的存活时间不宜过长,不然没办法保证最终一致性;也不宜过短,key频繁过期而使用分布式锁会带来不小的性能开销。



