为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落 盘工作。
哪些数据适合放入缓存?
举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率 来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。
data = cache.load(id);//从缓存加载数据
If(data == null){
data = db.load(id);//从数据库加载数据
cache.put(id,data);//保存到 cache 中
}
return data;
注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没 有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题。
一般使用hashmap或者redis进行缓存
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>redis.clientsgroupId>
<artifactId>jedisartifactId>
dependency>
spring:
redis:
host: 47.93.21.100
port: 6379
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Test
public void test(){
ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();
valueOperations.set("hollow","word");
String hollow = valueOperations.get("hollow");
System.out.println(hollow);
}
我们可以同时去一个地方“占坑”,如果占到,就执行逻辑。否则就必须等待,直到释放锁。 “占坑”可以去redis,可以去数据库,可以去任何大家都能访问的地方。 等待可以自旋的方式
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1. 分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if(lock){
//加锁成功
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败 重试 synchronize
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1. 分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111");
if(lock){
//加锁成功
//2. 设置过期时间
redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败 重试 synchronize
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
//1. 分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", "111",300,TimeUnit.SECONDS);
if(lock){
//加锁成功
//2. 设置过期时间
// redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
redisTemplate.delete("lock");
return dataFromDb;
}else {
//加锁失败 重试 synchronize
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
String uuid = UuidUtils.generateUuid().toString();
//1. 分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
//加锁成功
//2. 设置过期时间
// redisTemplate.expire("lock",30,TimeUnit.SECONDS);
Map<String, List<Catelog2Vo>> dataFromDb = getDataFromDb();
String lockValue = redisTemplate.opsForValue().get("lock");
if(lockValue.equals(uuid)) {
//删除自己的锁
redisTemplate.delete("lock");
}
return dataFromDb;
}else {
//加锁失败 重试 synchronize
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
public Map<String, List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock() {
String uuid = UuidUtils.generateUuid().toString();
//1. 分布式锁 去redis占坑
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock", uuid,300,TimeUnit.SECONDS);
if(lock){
//加锁成功
Map<String, List<Catelog2Vo>> dataFromDb = null;
try {
dataFromDb = getDataFromDb();
}finally {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
//删除锁
Long lock1 = redisTemplate.execute(new DefaultRedisScript<Long>(script, Long.class), Arrays.asList("lock"), uuid);
}
// String lockValue = redisTemplate.opsForValue().get("lock");
// if(lockValue.equals(uuid)) {
// //删除自己的锁
// redisTemplate.delete("lock");
// }
return dataFromDb;
}else {
//加锁失败 重试 synchronize
return getCatalogJsonFromDbWithRedisLock();//自旋的方式
}
}
Redisson 是架设在 Redis 基础上的一个Java驻内存数据网格(In-Memory Data Grid)。充分的利用了 Redis 键值数据库提供的一系列优势,基于 Java 实用工具包中常用接口,为使用者提供了一系列具有分布式特性的常用工具类。使得原本作为协调单机多线程并发程序的工 具包获得了协调分布式多机多线程并发系统的能力,大大降低了设计和研发大规模分布式系统的难度。同时结合各富特色的分布式服务,更进一步简化了分布式环境中程序相互之间的协作。 官方文档:https://github.com/redisson/redisson/wiki/%E7%9B%AE%E5%BD%95
<dependency>
<groupId>org.redissongroupId>
<artifactId>redissonartifactId>
<version>3.12.0version>
dependency>
@Configuration
public class MyRedissonConfig {
/**
* 所有对Redisson的使用都是通过RedissonClient对象
* @return
*/
@Bean(destroyMethod = "shutdown")
public RedissonClient redisson() throws IOException {
//1.创建配置
Config config = new Config();
config.useSingleServer().setAddress("redis://47.93.21.100:6379");
//根据config创建出RedissonClient示例
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
@SpringBootTest
class GulimallProductApplicationTests {
@Autowired
private RedissonClient redissonClient;
@Test
public void test1(){
System.out.println(redissonClient);
}
}
可重入锁是某个线程已经获得某个锁,可以再次获取锁而不会出现死锁。再次获取锁的时候会判断当前线程是否是已经加锁的线程,如果是对锁的次数+1,释放锁的时候加了几次锁,就需要释放几次锁。
基于Redis的Redisson分布式可重入锁RLock Java对象实现了java.util.concurrent.locks.Lock接口。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
@ResponseBody
@GetMapping("/hello")
public String hello(){
//1.获取一把锁,只要锁的名字一样,就是同一把锁
RLock lock = redisson.getLock("my-lock");
//2.加锁
lock.lock();//阻塞式等待 默认加的锁是30s
//1. 锁的自动续期 如果业务超长,运行期间自动给锁续上新的30s,不用担心业务时间长,锁自动过期会删除
//2.加锁的业务只要运行完成,就不会给当前锁续期,即使不手动解锁,锁默认在30s后自动删除
try {
System.out.println("加锁成功,执行业务"+Thread.currentThread().getId());
Thread.sleep(30000);
}catch (Exception e){
e.printStackTrace();
}finally {
//3.解锁
System.out.println("释放锁"+Thread.currentThread().getId());
lock.unlock();
}
return "hello";
}
大家都知道,如果负责储存这个分布式锁的Redisson节点宕机以后,而且这个锁正好处于锁住的状态时,这个锁会出现锁死的状态。为了避免这种情况的发生,Redisson内部提供了一个监控锁的看门狗,它的作用是在Redisson实例被关闭前,不断的延长锁的有效期。默认情况下,看门狗的检查锁的超时时间是30秒钟,也可以通过修改Config.lockWatchdogTimeout来另行指定。
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
特定
另外Redisson还通过加锁的方法提供了leaseTime的参数来指定加锁的时间。超过这个时间后锁便自动解开了。
lock.lock(10, TimeUnit.SECONDS);//10秒自动解锁,自动解锁时间一定要大于业务的执行时间
基于Redis的Redisson分布式可重入读写锁RReadWriteLock Java对象实现了java.util.concurrent.locks.ReadWriteLock接口。其中读锁和写锁都继承了RLock接口。
分布式可重入读写锁允许同时有多个读锁和一个写锁处于加锁状态。
@GetMapping("/write")
@ResponseBody
public String writeValue(){
String s="";
RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
RLock lock = readWriteLock.writeLock();
try {
//改数据加写锁 读数据加读锁
lock.lock();
s = UUID.randomUUID().toString();
Thread.sleep(30000);
redisTemplate.opsForValue().set("writeValue",s);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
return s;
}
@GetMapping("/read")
@ResponseBody
public String readValue(){
String s="";
RReadWriteLock writeLock = redisson.getReadWriteLock("rw-lock");
//加读锁
Lock rLock = writeLock.readLock();
try {
rLock.lock();
s = redisTemplate.opsForValue().get("writeValue").toString();
Thread.sleep(30000);
}catch (Exception e){
e.printStackTrace();
}finally {
rLock.unlock();
}
return s;
}
结论
基于Redis的Redisson的分布式信号量(Semaphore)Java对象RSemaphore采用了与java.util.concurrent.Semaphore相似的接口和用法。同时还提供了异步(Async)、反射式(Reactive)和RxJava2标准的接口。
这里以停车位为例,当停车时,获取一个信号量,获取到信号量之后进行停车,车开走之后可以在释放一个信号量
/**
* 车库停车
* @return
* @throws InterruptedException
* 信号量 可以用作分布式限流
*/
@GetMapping("/park")
@ResponseBody
public String park() throws InterruptedException {
RSemaphore park = redisson.getSemaphore("park");
park.acquire();//获取一个信号量,获取一个信号量占一个车位
return "ok";
}
@GetMapping("/go")
@ResponseBody
public String go(){
RSemaphore park = redisson.getSemaphore("park");
park.release();//释放一个车位
return "ok";
}
闭锁相当于一扇门,在闭锁到达结束状态之前,这扇门一直是关闭着的,没有任何线程可以通过,当到达结束状态时,这扇门才会打开并容许所有线程通过。它可以使一个或多个线程等待一组事件发生。闭锁状态包括一个计数器,初始化为一个正式,正数表示需要等待的事件数量。countDown方法递减计数器,表示一个事件已经发生,而await方法等待计数器到达0,表示等待的事件已经发生。CountDownLatch强调的是一个线程(或多个)需要等待另外的n个线程干完某件事情之后才能继续执行。
10个运动员准备赛跑,他们等待裁判一声令下就开始同时跑,当最后一个人通过终点的时候,比赛结束。10个运动相当于10个线程,这里关键是控制10个线程同时跑起来,还有怎么判断最后一个线程到达终点。可以用2个闭锁,第一个闭锁用来控制10个线程等待裁判的命令,第二个闭锁控制比赛结束。
5个班放学,当5个班的同学都走完之后,锁门
@GetMapping("/lockDoor")
@ResponseBody
public String lockDoor() throws InterruptedException {
RCountDownLatch door = redisson.getCountDownLatch("door");
door.trySetCount(5);
door.await();
return "放假了";
}
@GetMapping("/gogogo/{id}")
@ResponseBody
public String gogogo(@PathVariable("id") Long id){
RCountDownLatch door = redisson.getCountDownLatch("door");
door.countDown();//计算减一
return id+"班的人走完了";
}
• 无论是双写模式还是失效模式,都会导致缓存的不一致问题。即多个实例同时更新会出事。怎么办?
• 总结:
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-cacheartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
spring.cache.type=redis
@Cacheable | 触发将数据保存到缓存的操作 |
---|---|
@CacheEvict | 触发数据从缓存删除的操作 |
@CachePut | 不影响方法执行更新缓存 |
@Caching | 组合以上多个操作 |
@CacheConfig | 在类级别共享缓存的相同配置 |
@EnableCaching
@MapperScan("com.atguigu.gulimall.product.dao")
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.atguigu.gulimall.product.feign")
@EnableCaching
public class GulimallProductApplication {
public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}
}
//每一个需要缓存的数据我们都来指定要放到那个名字的缓存【缓存的分区(按照业务类型分)】
@Cacheable({"category"}) //代表当前方法的结果需要进行缓存,如果缓存中有方法不用调用,缓存中没有会调用方法,最后将方法的结果存入缓存
@Override
public List<CategoryEntity> getLevel1Categorys() {
QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_cid",0);
List<CategoryEntity> list = baseMapper.selectList(queryWrapper);
return list;
}
@Cacheable(value = {"category"},key = "'level'")
@Override
public List<CategoryEntity> getLevel1Categorys() {
QueryWrapper<CategoryEntity> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("parent_cid",0);
List<CategoryEntity> list = baseMapper.selectList(queryWrapper);
return list;
}
spring.cache.redis.time-to-live=60000
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
// @Autowired
// CacheProperties cacheProperties;
/**
* 配置文件中的东西没有用上
* @ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties {
* @return
*/
@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.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
配置参数
spring.cache.type=redis
spring.cache.redis.time-to-live=60000
# 如果指定了前缀就用指定的前缀,如果没有就默认使用缓存的名字作为前缀
spring.cache.redis.key-prefix=CACHE
# 是否使用缓存前缀 默认为TRUE
spring.cache.redis.use-key-prefix=true
# 是否缓空值 防止缓存穿透
spring.cache.redis.cache-null-values=true
/**
* 级联更新所有关联的数据
* 缓存失效模式
* 1.同时进行多种操作 @caching
* 2.指定删除某个分区下的所有数据 @CacheEvict(value = {"category"},allEntries = true)
* 3.存储同一类型的数据,都可以指定成同一分区,分区名默认就是缓存的前缀
* @param category
*/
@Override
// @Caching(
// evict = {
// @CacheEvict(value = {"category"},key = "'getLevel1Categorys'"),
// @CacheEvict(value = {"category"},key = "'getCatalogJson'")
// }
// )
@CacheEvict(value = {"category"},allEntries = true)
@Transactional
public void updateDetail(CategoryEntity category) {
//修改分类
updateById(category);
//修改其他的分类信息
categoryBrandRelationService.updateCategpry(category.getCatId(),category.getName());
// TODO
}
缓存失效模式
@caching
@CacheEvict(value = {"category"},allEntries = true)
@Cacheable(value = {"category"},key = "#root.method.name",sync = true)
只有@Cacheable可以