缓存
缓存穿透:查询一个不存在的数据,由于缓存不命中,将大量查询数据库,但是数据库也没有此记录。
没有将这次查询的null写入缓存,导致了这个不存在的数据每次请求都要到存储层查询,失去了缓存的意义。
解决:null结果缓存,并加入短暂的过期事件
缓存雪崩:设置缓存时采用了相同的过期时间,导致缓存存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重
解决:原有的失效时间基础上增加一个随机值,这样缓存的过期时间重复率就会降低。
缓存击穿:对于一些设置了过期时间的key,如果这些key在某些时间点高并发地访问,同时正好失效,那么所有的压力都会到数据库上。
解决:大量并发只让一个去查,其他人等待,查到以后释放锁,其他人获取到锁,先查缓存,就有数据,不用去数据库。
将redis作为缓存工具
①、模块中导入依赖,并且配置文件application.yml添加reids连接信息IP和端口
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②、Service业务类中使用
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Map<String,List<Categlo2Vo>> getCatelogJson(){
String catelogJSON = redisTemplate.opsForValue().get("catelogJSON");
if(!StringUtils.isEmpty(catalogJSON)){
//如果redis不存在,则从数据库中查找,然后将数据库查找的结果放入缓存redis中
Map<String,List<Catelog2Vo>> catalogJsonFromDb = getCatalogJsonFromDb();
return catalogJsonFromDb;
}
//如果缓存中存在json,则需要转为对象
Map<String,List<Catelog2Vo>> result = JSON.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
return result;
}
//查询数据库
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDb(){
synchronized(this){
//得到锁以后,应该再去缓存确定一次,如果没有才需要继续查询
String catalogJSON = redisTemplate.opsForValue().get("catalogJSON");
if(!StringUtils.isEmpty(catalogJSON)){
Map<String,List<Catelog2Vo>> result = JSON
.parseObject(catalogJSON,new TypeReference<Map<String,List<Catelog2Vo>>>(){});
return result;
}
//查询数据库的业务逻辑……
//...
//缓存中以json类型存储可以跨平台使用,所以将对象转为json,并放到缓存中
String s = JSON.toJSONString(catalogJsonFromDb);
redisTemplate.opsForValue().set("catalogJSON",s,1,TimeUnit.DAY);
return parent_cid;
}
}
产生堆外异常OutOfDirectMemoryError
SpringBoot2.0以后默认使用lettuce作为操作redis的客户端,使用netty进行网络通信
lettuce的bug导致netty堆外内存溢出,-Xmx300m
解决方案:①、升级lettuce客户端 ②、切换jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core<artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</denpendency>
分布式锁
redis命令
set locl 1111 NX
set lock 1111 EX 300 NX #占坑的同时添加了过期时间
①、若查询数据库业务中出现异常,导致delete锁无法执行,出现死锁情况,则通过添加expire过期时间来释放锁
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock(){
//占分布式锁(如果不存在,即设置)
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","111");
if(lock){//加锁成功,执行业务
redisTemplate.expire("lock",30,TimeUnit.SECONDS);//防止查询数据库业务中出现异常导致的死锁
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();//即使业务出现异常,还会删除锁
redisTemplate.delete("lock");
return dataFromDb
}else{ //加锁失败
//重试
return getCatalogJsonFromDbWithRedisLock();
}
}
②、若在执行过期时间的时候出现异常,造成无法释放锁
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock(){
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock","1111",300,Time.SECONDS);//占锁的时候同时设置过期时间
if(lock){
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();//即使业务出现异常,还会删除锁
redisTemplate.delete("lock");
return dataFromDb
}else{
return getCatalogJsonFromDbWithRedisLock();
}
}
③、删除锁导致将其他线程锁也删除,通过判断先获取自己的锁值,然后在删除自己的锁
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock(){
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,Time.SECONDS);//占锁的时候同时设置过期时间
if(lock){
Map<String,List<Catelog2Vo>> dataFromDb = getDataFromDb();
String lockValue = redisTemplate.opsForValue().get("lock");
if(uuid.equals(lockValue)){
redisTemplate.delete("lock");
}
return dataFromDb;
}else{
return getCatalogJsonFromDbWithRedisLock();
}
}
④、如果判断之后出现锁过期,而其他线程设置了锁,那么删除的依然是其他线程的锁(加锁、解锁都保证原子性)
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDbWithRedisLock(){
String uuid = UUID.randomUUID().toString();
Boolean lock = redisTemplate.opsForValue().setIfAbsent("lock",uuid,300,Time.SECONDS);//占锁的时候同时设置过期时间
if(lock){
Map<String,List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally{
//通过lua脚本解锁
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,Integer.class),
Arrays.asList("lock"),uuid);
}
return dataFromDb;
}else{
return getCatalogJsonFromDbWithRedisLock();
}
}
分布式锁Redisson
①、导入依赖
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.0</version>
</dependency>
②、配置类
@Configuration
public class MyRedissonConfig{
@Bean(destroyMethod="shutdown")
public RedissonClient redisson() throws IOException{
Config config = new Config();
/**
Redis集群模式
config.useClusterServers()
.addNodeAddress("127.0.0.1:7004","127.0.0.1:7001");
*/
//Redis单节点模式
config.useSingleServer().setAddress("redis://192.168.56.10:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
③、使用
@Autowired
private RedissonClient redisson;
public Map<String,List<Catelog2Vo>> getCatalogJsonFromDbWithRedissonLock(){
//获取锁,锁的粒度越小越好
RLock lock = redisson.getLock("catalogJson-lock");
lock.lock();
Map<String,List<Catelog2Vo>> dataFromDb;
try{
dataFromDb = getDataFromDb();
}finally{
lock.unlock();
}
return dataFromDb;
}
缓存一致性
整合SpringCache
①、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②、配置application.properties,配置使用redis作为缓存
spring.cache.type=redis
spring.cache.redis.time-to-live=1000 #设置过期时间,以毫秒为单位
spring.cache.redis.key-prefix=CACHE_
spring.cache.redis.use-key-prefix=true
#是否缓存空值(防止缓存穿透)
spring.cache.redis.cache-null-value=true
③、使用
先在启动类开启缓存注解 @EnableCaching
Service实现的业务类中:
@Cacheable(value={"category"},key="'level1Categorys'") //当前方法的结果需要缓存,如果缓存中有,方法不用调用
@Override
public List<CategoryEntity> getLevel1Categorys(){
Long l = System.currentTimeMllis();
List<CategoryEntity> categoryEntities = baseMapper.selectList();
return categoryEntities;
}
@CacheEvict(value="category",key="getLevel1Categorys")//修改时,删除缓存
@Transactional
@Override
public void updateCascade(CategoryEntity category){
}
自定义缓存配置类
@EnableConfigurationProperties(CacheProperties.class) //开启和配置文件的绑定功能,使得配置文件的内容生效
@Configuration
@EnableCaching
public class MyCacheConfig{
/**
第一种方式:直接从容器的配置文件获取
第二种方式:将其作为方法参数(如下方法的参数)
*/
// @Autowired
// CacheProperties cacheProperties;//从容器中获取配置
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//对key和value进行序列化
config = config.serializeKeysWith(RedisSerializetionContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
config = config.serializeValuesWith(RedisSerializetionContext.SerializationPair.fromSerializer(new StringRedisSerializer(new GenericJackson2JsoRedisSerializer()));
//将配置文件中的所有配置生效
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if(redisProperties.getTimeToLive()!=null){
config = config.entityTtl(redisProperties.getTimeToLive());
}
if(redisProperties.getKeyPrefix()!=null){
config = config.prefixKeysWith(redis.getKeyPrefix());
}
if(!redisProperties.isCacheNullValues()){
config = config.disableCachingNullValues();
}
if(!redisProperties.isUserKeyPrefix){
config = config.disableKeyPrefix();
}
return config;
}
}
分页插件
查询用户分页显示功能
①、myatis分页插件依赖
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper</artifactId>
</dependency>
②、配置文件spring-persist-mybtis.xml
<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactory">
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<property name="mapperLocations" value="classpath:mybatis/mapper/*Mapper.xml"/>
<property name="dataSource" ref="dataSource"/>
<!--配置插件-->
<property name="plugins">
<array>
<!--分页插件-->
<bean class="com.github.pagehelper.PageHelper">
<property name="properties">
<props>
<!--数据库方言-->
<prop key="dialect">mysql</prop>
<!--配置页码的合理化修正,在1~总页数之间修正页码-->
<prop key="reasonable">true</prop>
</props>
</property>
</bean>
</array>
</property>
</bean>
③、Mapper文件和Mapper接口
<select id="selectAdminByKeyword" resultMap="BaseResultMap">
select id,login_acct,user_pswd,user_name,email,create_time
from t_admin
where
login_acct like concat("%",#{keyword},"%") or
user_name like concat("%",#{keyword},"%") or
email like concat("%",#{keyword},"%")
</select>
List<Admin> selectAdminByKeyword(String keyword);
④、service实现
@Override
public PageInfo<Admin> getPageInfo(String keyword,Integer pageNum,Integer pageSize){
//调用PageHelper静态方法开启分页功能
PageHelper.startPage(pageNum,pageSize);
//执行查询
List<Admin> list = adminMapper.selectAdminByKeyword(keyword);
//封装到PageInfo对象中
return new PageInfo<>(list);
}
⑤、Controller
@RequestMapping("/admin/get/page.html")
public String getPageInfo(@RequestParam(value="keyword",defaultValue="")String keyword,
@RequestParam(value="pageNum",defaultValue="1")Integer pageNum,
@RequestParam(value="pageSize",defaultValue="5")Integr pageSize,
ModelMap modelMap){
PageInfo<Admin> pageInfo = adminService.getPageInfo(keyword,pageNum,pageSize);
modelMap.addAttribute(CrowdConstant.ATTR_NAME_PAGE_INFO,pageInfo);
return "admin-page"
}
⑥、页面中循环遍历
<c:if test="${empty requestScope.pageInfo.list}">
<tr>
<td colspan="6" align="center">没有查询到你要的数据</td>
</tr>
</c:if>
<c:if test="${!empty requestScope.pageInfo.list}">
<c:forEach items="${requestScope.pageInfo.list}" var="admin">
<c:forEach>
</c:if>