目录
缓存使用
本地缓存
分布式缓存
缓存穿透
缓存雪崩
缓存击穿
缓存数据一致性
双写模式
失效模式
canal订阅binlog的方式
总结
缓存加锁
锁-时序问题
本地锁:只能锁住当前进程
synchronized
juc(lock)
分布式锁
redis分布式锁
redison分布式锁
SpringBoot整合redis
docker安装redis
引入spring-boot-starter-data-redis
配置redis
SpringBoot整合redisson
引入redisson
配置redisson
Redisson-lock看门狗原理
SpringBoot整合springcache
引入spring-boot-starter-cache
配置springcache
缓存注解
springcache原理
缓存使用
本地缓存
分布式缓存
缓存穿透
缓存雪崩
缓存击穿
缓存数据一致性
双写模式
失效模式
canal订阅binlog的方式
总结
- 能放入缓存的数据本就不应该是实时性、一致性要求超高的,缓存数据的时候加上过期时间,保证可以拿到最新数据即可。
- 不应该过度设计,增加系统的复杂性,并发读写,写写的时候,使用读写锁。
- 遇到实时性、一致性要求高的数据,就应该查数据库,即使慢点。
缓存加锁
锁-时序问题
本地锁:只能锁住当前进程
synchronized
public Map<String, List<Category2VO>> getCategorySync() {
Map<String, List<Category2VO>> map;
synchronized (this) {
// 从redis中获取数据
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
System.out.println("getCatelogSync-->缓存没命中,查询数据库。。。。。。");
map = getCatelogFromDB();
// 将数据放入redis
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(map), 30, TimeUnit.DAYS);
} else {
System.out.println("getCatelogSync-->缓存命中。。。。。。");
map = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Category2VO>>>(){});
}
}
return map;
}
juc(lock)
- 可重入锁(ReentrantLock)
public String reentrantLock() {
Lock lock = new ReentrantLock();
lock.lock();
System.out.println("加锁成功。。。。。。"+Thread.currentThread().getId());
String s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("ww", s);
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("释放锁成功。。。。。。"+Thread.currentThread().getId());
}
return s;
}
- 读写锁(ReentrantReadWriteLock)
public String writeLock() {
Lock lock = new ReentrantReadWriteLock().writeLock();
lock.lock();
System.out.println("写锁加锁成功。。。。。。"+Thread.currentThread().getId());
String s = UUID.randomUUID().toString();
redisTemplate.opsForValue().set("ww", s);
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("写锁释放锁成功。。。。。。"+Thread.currentThread().getId());
}
return s;
}
public String readLock() {
Lock lock = new ReentrantReadWriteLock().readLock();
lock.lock();
System.out.println("读锁加锁成功。。。。。。"+Thread.currentThread().getId());
String s = redisTemplate.opsForValue().get("ww");
try {
Thread.sleep(30000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
System.out.println("读锁释放锁成功。。。。。。"+Thread.currentThread().getId());
}
return s;
}
- 闭锁(CountDownLatch)
public void countDownLatch() {
CountDownLatch countDownLatch = new CountDownLatch(5);
try {
// 计数减一
countDownLatch.countDown();
// 等待闭锁完成
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
- 信号量(Semaphore)
public void semaphore() {
// 声明信号量,5个信号
Semaphore s = new Semaphore(5); // 声明信号量,5个信号
try {
// 获取一个信号量,阻塞式等待
s.acquire();
// 释放一个信号
s.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
分布式锁
redis分布式锁
- 加锁命令:SET key value [EX seconds] [PX milliseconds] [NX|XX]
-
EX
seconds – 设置键key的过期时间,单位时秒 -
PX
milliseconds – 设置键key的过期时间,单位时毫秒 -
NX
– 只有键key不存在的时候才会设置key的值 -
XX
– 只有键key存在的时候才会设置key的值
- luo脚本:
if redis.call("get",KEYS[1]) == ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end
/**
* redis分布式锁:加锁+设置过期时间+删锁=原子性
*/
public Map<String, List<Category2VO>> getCategoryByRedis() {
Map<String, List<Category2VO>> map = null;
// 1、加锁+设置过期时间,保证原子性
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
while (true) { // 自旋的方式
if (lock) {
try {
// 从redis中获取数据
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
System.out.println("getCatelogByRedis-->缓存没命中,查询数据库。。。。。。");
map = getCatelogFromDB();
// 将数据放入redis
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(map), 30, TimeUnit.DAYS);
} else {
System.out.println("getCatelogByRedis-->缓存命中。。。。。。");
map = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Category2VO>>>(){});
}
} finally {
// 2、luo脚本删除锁,保证原子性
// KEYS[1]=lock;ARGV[1]=uuid
String script = "if redis.call(\"get\",KEYS[1]) == ARGV[1] then return redis.call(\"del\",KEYS[1]) else return 0 end";
redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
return map;
} else {
// 加锁失败,重试
lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid, 300, TimeUnit.SECONDS);
}
}
}
redison分布式锁
/**
* redisson分布式锁
*/
public Map<String, List<Category2VO>> getCategoryByRedisson() {
Map<String, List<Category2VO>> map;
RLock lock = redisson.getLock("catalogJSON-lock");
lock.lock();
try {
// 从redis中获取数据
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if (StringUtils.isEmpty(catalogJSON)) {
System.out.println("getCatelogByRedisson-->缓存没命中,查询数据库。。。。。。");
map = getCatelogFromDB();
// 将数据放入redis
redisTemplate.opsForValue().set("catalogJSON", JSON.toJSONString(map), 30, TimeUnit.DAYS);
} else {
System.out.println("getCatelogByRedisson-->缓存命中。。。。。。");
map = JSON.parseObject(catalogJSON, new TypeReference<Map<String, List<Category2VO>>>(){});
}
} finally {
lock.unlock();
}
return map;
}
SpringBoot整合redis
docker安装redis
- 由于docker redis容器内部没有redis.conf文件,我们现在外部创建出redis.conf文件,然后再和docker容器内部进行挂载
mkdir -p /mydata/redis/conf
touch /mydata/redis/conf/redis.conf
docker run -p 6379:6379 --name redis \
-v /mydata/redis/data:/data \
-v /mydata/redis/conf/redis.conf:/etc/redis/redis.conf \
-d redis redis-server /etc/redis/redis.conf
vi redis.conf
# 启用auf持久化
appendonly yes
引入spring-boot-starter-data-redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置redis
spring.redis.host=localhost
spring.redis.port=6379
SpringBoot整合redisson
引入redisson
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
</dependency>
配置redisson
@Configuration
public class RedissonConfig {
@Value("${idea.redis.host}")
private String redisHost;
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
Config config = new Config();
config.useSingleServer().setAddress(redisHost);
return Redisson.create(config);
}
}
Redisson-lock看门狗原理
- 如果我们设置了锁的过期时间,就发送redis执行脚本,进行占锁,超时时间就是我们指定的时间。
- 如果我们没有设置锁的过期时间,就使用看门狗默认时【lockWatchdogTimeout=30000L】,只要占锁成功,就会启动一个定时任务【重新设置过期时间,新的过期时间也是看门狗默认时间】,每隔10s【this.internalLockLeaseTime / 3L】都会自动续期过期时间,续成默认看门狗时间。
private void lock(long leaseTime, TimeUnit unit, boolean interruptibly) throws InterruptedException {
long threadId = Thread.currentThread().getId();
// 占锁
Long ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl != null) {
RFuture<RedissonLockEntry> future = this.subscribe(threadId);
if (interruptibly) {
this.commandExecutor.syncSubscriptionInterrupted(future);
} else {
this.commandExecutor.syncSubscription(future);
}
try {
while(true) {
ttl = this.tryAcquire(leaseTime, unit, threadId);
if (ttl == null) {
return;
}
if (ttl.longValue() >= 0L) {
try {
((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl.longValue(), TimeUnit.MILLISECONDS);
} catch (InterruptedException var13) {
if (interruptibly) {
throw var13;
}
((RedissonLockEntry)future.getNow()).getLatch().tryAcquire(ttl.longValue(), TimeUnit.MILLISECONDS);
}
} else if (interruptibly) {
((RedissonLockEntry)future.getNow()).getLatch().acquire();
} else {
((RedissonLockEntry)future.getNow()).getLatch().acquireUninterruptibly();
}
}
} finally {
this.unsubscribe(future, threadId);
}
}
}
// 占锁
private Long tryAcquire(long leaseTime, TimeUnit unit, long threadId) {
return (Long)this.get(this.tryAcquireAsync(leaseTime, unit, threadId));
}
private <T> RFuture<Long> tryAcquireAsync(long leaseTime, TimeUnit unit, long threadId) {
if (leaseTime != -1L) { // 设置了过期时间
return this.tryLockInnerAsync(leaseTime, unit, threadId, RedisCommands.EVAL_LONG);
} else { // 没有设置过期时间
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
}
}
// 设置了过期时间,发送redis执行脚本
<T> RFuture<T> tryLockInnerAsync(long leaseTime, TimeUnit unit, long threadId, RedisStrictCommand<T> command) {
this.internalLockLeaseTime = unit.toMillis(leaseTime);
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, command, "if (redis.call('exists', KEYS[1]) == 0) then redis.call('hset', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('hincrby', KEYS[1], ARGV[2], 1); redis.call('pexpire', KEYS[1], ARGV[1]); return nil; end; return redis.call('pttl', KEYS[1]);", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
// 未设置过期时间,给redis发送命令设置锁的过期时间
RFuture<Long> ttlRemainingFuture = this.tryLockInnerAsync(this.commandExecutor.getConnectionManager().getCfg().getLockWatchdogTimeout(), TimeUnit.MILLISECONDS, threadId, RedisCommands.EVAL_LONG);
// 监听占锁结果
ttlRemainingFuture.onComplete((ttlRemaining, e) -> {
if (e == null) {
if (ttlRemaining == null) {
//
this.scheduleExpirationRenewal(threadId);
}
}
});
return ttlRemainingFuture;
private void scheduleExpirationRenewal(long threadId) {
RedissonLock.ExpirationEntry entry = new RedissonLock.ExpirationEntry();
RedissonLock.ExpirationEntry oldEntry = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.putIfAbsent(this.getEntryName(), entry);
if (oldEntry != null) {
oldEntry.addThreadId(threadId);
} else {
entry.addThreadId(threadId);
this.renewExpiration(); // 重新设置过期时间
}
}
private void renewExpiration() {
RedissonLock.ExpirationEntry ee = (RedissonLock.ExpirationEntry)EXPIRATION_RENEWAL_MAP.get(this.getEntryName());
if (ee != null) {
Timeout task = this.commandExecutor.getConnectionManager().newTimeout(new TimerTask() {
public void run(Timeout timeout) throws Exception {
RedissonLock.ExpirationEntry ent = (RedissonLock.ExpirationEntry)RedissonLock.EXPIRATION_RENEWAL_MAP.get(RedissonLock.this.getEntryName());
if (ent != null) {
Long threadId = ent.getFirstThreadId();
if (threadId != null) {
RFuture<Boolean> future = RedissonLock.this.renewExpirationAsync(threadId.longValue());
future.onComplete((res, e) -> {
if (e != null) {
RedissonLock.log.error("Can't update lock " + RedissonLock.this.getName() + " expiration", e);
} else {
if (res.booleanValue()) {
RedissonLock.this.renewExpiration();
}
}
});
}
}
}
}, this.internalLockLeaseTime / 3L, TimeUnit.MILLISECONDS);
ee.setTimeout(task);
}
}
// 异步重新设置过期时间
protected RFuture<Boolean> renewExpirationAsync(long threadId) {
return this.commandExecutor.evalWriteAsync(this.getName(), LongCodec.INSTANCE, RedisCommands.EVAL_BOOLEAN, "if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then redis.call('pexpire', KEYS[1], ARGV[1]); return 1; end; return 0;", Collections.singletonList(this.getName()), new Object[]{this.internalLockLeaseTime, this.getLockName(threadId)});
}
SpringBoot整合springcache
引入spring-boot-starter-cache
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置springcache
spring.cache.type=redis
# 毫秒为单位
spring.cache.redis.time-to-live=3600000
# 缓存空值,防止缓存穿透
spring.cache.redis.cache-null-values=true
@EnableCaching // 开启缓存功能
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
public class RedisCacheConfig {
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
// 将配置文件中的所有配置都生效
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
缓存注解
- @EnbaleCaching:开启缓存功能
- @Cacheable:触发将数据保存到缓存的操作,如果缓存中有,方法不会调用,如果缓存中没有,会调用方法,最后将方法返回结果放入缓存
- @CacheEvict:触发将数据从缓存中删除(失效模式)
- @CachePut:不影响方法执行更新缓存(双写模式)
- @Cacheing:组合以上多个操作
- @CacheConfig:在类级别共享缓存相同配置
springcache原理
- spring从3.1开始定义了Cache和Cachemanager接口来统一不同的缓存技术