Redis缓存实战-使用Spring AOP和Redis实现Dao层查询缓存优化实战

时间:2024-10-25 21:50:39

前言

通过SpringAOP技术,动态拦截dao层所有的查询操作,并把mysql数据库中的数据缓存到redis中。下次再次进行同样的查询操作时,从redis中获取数据。一旦进行了增删改操作,需要清除redis缓存。

 准备工作

基于SSM项目,缓存前端分页查询数据等操作,如何搭建ssm项目这里不在赘述。

基于AOP使用动态代理,增强功能,拦截所有的查询sql,并做增强操作,需要借助SpringAOP功能,想要使用redis实现缓存业务,需要添加redis和spring整合的相关依赖:

添加对应核心依赖

<!--aop-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-aop</artifactId>
      <version>4.3.3.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.aspectj</groupId>
      <artifactId>aspectjweaver</artifactId>
      <version>1.9.1</version>
    </dependency>

  <!--jedis-->
    <dependency>
      <groupId>redis.clients</groupId>
      <artifactId>jedis</artifactId>
      <version>2.9.0</version>
    </dependency>
    <!--spring-data-redis-->
    <dependency>
      <groupId>org.springframework.data</groupId>
      <artifactId>spring-data-redis</artifactId>
      <version>1.7.2.RELEASE</version>
    </dependency>

 准备好对应的三层架构Mybatis,Spring,SpringMVC

 

这里演示的是在以往的ssm项目添加redis缓存功能,主要介绍redis作为缓存使用的应用特点,其他代码不再赘述。

redis实现缓存功能

准备工具类

使用Spring提供的模版类RedisTemplate来操作redis,因此操作redis的相关代码固定,且需要重复书写,所以将操作redis的代码封装成共用的工具类。

RedisUtilNew

@Component
public class RedisUtilNew {

    private static ApplicationContext context =new ClassPathXmlApplicationContext("spring/spring-mybatis.xml");
    private static RedisTemplate redisTemplate =context.getBean(RedisTemplate.class);

//从redis中获取分页数据
    public static Page getPage(int currentPage,User user){
        String name = user.getName();
        Integer age = user.getAge();
        if (name.isEmpty()){
            name="all";
        }
        if (age==null){
            age=0;
        }
        Page page = (Page) redisTemplate.opsForValue().get("page:"+currentPage+":username:"+name+":age:"+age);
        return page;
    }

//向redis中设置分页数据
    public static void setPage(Page page,int currentPage,User user){
        String name = user.getName();
        Integer age = user.getAge();
        if (name.isEmpty()){
            name="all";
        }
        if (age==null){
            age=0;
        }
        redisTemplate.opsForValue().set("page:"+currentPage+":username:"+name+":age:"+age,page,60*24, TimeUnit.MINUTES);

    }

//从redis中获用户数据
    public static User getUser(User user){
        String name = user.getName();
        Integer age = user.getAge();
        if (name.isEmpty()){
            name="all";
        }
        if (age==null){
            age=0;
        }
        User u = (User) redisTemplate.opsForValue().get("user:"+name+":"+age);
        return u;
    }
//向redis中设置用户数据
    public static void setUser(User user){
        String name = user.getName();
        Integer age = user.getAge();
        if (name.isEmpty()){
            name="all";
        }
        if (age==null){
            age=0;
        }
        redisTemplate.opsForValue().set("user:"+name+":"+age,user,60*24, TimeUnit.MINUTES);
    }

    public static void deletes(){
        Set keys = redisTemplate.keys("*");
        redisTemplate.delete(keys);
    }

//删除redis中的数据
    public static void delete(User user){
        String name = user.getName();
        Integer age = user.getAge();
        if (name.isEmpty()){
            name="all";
        }
        if (age==null){
            age=0;
        }
        redisTemplate.delete("user:"+name+":"+age);
        Set<String> keys = redisTemplate.keys("*");
        for (String key:keys){
            if (key.contains("username:"+name+":age:"+age)){
                redisTemplate.delete(key);
            }
        }

        //redisTemplate.delete("page:"+currentPage+":username:"+name+":age:"+age);
    }

}

需求分析

我们想要使用redis实现缓存,本质上是在原有的ssm项目添加新的功能,如果直接将缓存操作写在原有代码中(修改了原有代码的结构),例如放在controller层中,就破坏了“无侵入式编程的思想”。我们使用Spring框架的一大特点,就是分层解耦和无侵入式编程,因此我们可以利用SpringAOP的功能,横切的给代码添加功能,既不破坏原有代码功能,又添加了缓存的新功能。

 AOP实现

 RedisQueryAdvice

我们只需对查询操作添加缓存,对于增删改是没有必要进行缓存的(但是需要更新缓存,否则导致数据不一致现象)

@Aspect
@Component
public class RedisQueryAdvice {

    @Pointcut("execution(* com.csx.service.impl.UserServiceImpl.queryLikeUsers(..))")
    public void pt1(){}

    @Pointcut("execution(* com.csx.service.impl.UserServiceImpl.getUser(..))")
    public void pt2(){}

    //新增
    @Pointcut("execution(* com.csx.service.impl.UserServiceImpl.addUser(..))")
    public void addPoint(){}
    //修改
    @Pointcut("execution(* com.csx.service.impl.UserServiceImpl.changeUser(..))")
    public void updatePoint(){}
    //删除
    @Pointcut("execution(* com.csx.service.impl.UserServiceImpl.removeUser(..))")
    public void deletePoint(){}

    @Autowired
    private UserService userService;

    @Around("pt1()")
    public Object queryLikeUsers(ProceedingJoinPoint pjp) throws Throwable{
        System.out.println("测试aop");
        Object[] args = pjp.getArgs();
        Map<String, Object> map = (Map<String, Object>) args[0];
        int currentPageNum = Integer.parseInt(String.valueOf(map.get("pageNum")));
        User user = (User) map.get("user");
        System.out.println(user);
        User u = RedisUtilNew.getUser(user);
        Page page=null;

        if (u!=null && u.getName().equals(user.getName())&&u.getAge()==user.getAge()){
            page=RedisUtilNew.getPage(currentPageNum,u);
            if (page!=null){
                System.out.println("redis取值");
                return page;
            }else {
                page = (Page)pjp.proceed();
                System.out.println("redis存值");
                RedisUtilNew.setPage(page,currentPageNum,user);
                return page;
            }
        }else {
            RedisUtilNew.setUser(user);
            if (page!=null){
                System.out.println("redis取值");
                return page;
            }else {
                page = (Page)pjp.proceed();
                System.out.println("redis存值");
                RedisUtilNew.setPage(page,currentPageNum,user);
                return page;
            }
        }




    }

    @Around("addPoint() ||updatePoint() ||deletePoint()")
    public Object removeRedis(ProceedingJoinPoint pjp) throws Throwable {

        System.out.println("执行增删改");
        RedisUtilNew.deletes();
      //  RedisUtilNew.delete(user);
        return pjp.proceed();
    }



    @Around("pt2()")
    public Object getUser(ProceedingJoinPoint pjp) throws Throwable{
         User  u = (User)pjp.proceed();

        User user = RedisUtilNew.getUser(u);

        if (user!=null){
            System.out.println("aop执行");
            return user;
        }else {
            RedisUtilNew.setUser(u);
            return u;
        }

    }


}
  • 使用环绕通知,动态拦截方法的调用
  • 书写切入点时,要保证能够匹配到对应的连接点,可以使用断点辅助调试
  • 一旦进行了增删改操作,则删除所有缓存,如果还是针对某个分页页面删除,则太过麻烦
  • 使用缓存是需要设置有效期,我设置为30分钟,防止缓存数据过多,占用大量内存

Service层实现

添加缓存功能,属于业务层的需求,因此我们要将该功能拦截到service方法,下面给出的service代码还是原来ssm项目的中代码,仅仅展示,本质上是拦截器进行拦截service方法进行增强

UserService

public interface UserService {

    //分页查询
    Page queryLikeUsers(Map<String,Object> cond);

    //新增用户
    int addUser(User user);
    //修改用户
    int changeUser(User user);
    //删除用户
    int removeUser(User user);
    User  getUser(User user);


}

UserServiceImpl

@Service(value = "userService")
public class UserServiceImpl implements UserService {
    @Autowired
    private UserDao userDao;

    @Override
    public Page queryLikeUsers(Map<String, Object> cond) {
        Page page = new Page();
//        //查询记录总数
//    page.setTotal(userDao.getLikeUsersCount(cond));
//        //每页大小(每页记录数)
        int pageSize = Integer.parseInt(String.valueOf(cond.get("pageSize")));
//        page.setPageSize(pageSize);
//        //当前页
        int currentPageNum = Integer.parseInt(String.valueOf(cond.get("pageNum")));
//        page.setPageNum(currentPageNum);
        //开启分页
        page = PageHelper.startPage(currentPageNum, pageSize);
//        List<User> list = userDao.getLikeUsers(cond);
        userDao.getLikeUsers(cond);
        return page;
    }

    @Override
    public int addUser(User user) {
        return userDao.insertUser(user);
    }

    @Override
    public int changeUser(User user) {
        return userDao.updateUser(user);
    }

    @Override
    public int removeUser(User user) {
        return userDao.delUser(user);
    }

    @Override
    public User getUser(User user) {
        return userDao.selectUserById(user);
    }

    
}

效果演示

刚开始缓存中不存在数据:

浏览器分页查看数据时:

将当前页数据存入redis中:

图形化界面:

命令行界面:

查询多次,和进行模糊查询:

可以看到中文数据变成乱码,因为我设置了序列化器,将键使用string序列化器,将值设置为jdk序列化器

扩展

spring-mybtais.xml序列化器配置

spring-mybatis.xml

 <!--redis缓存配置-->
    <bean id="poolConfig" class="redis.clients.jedis.JedisPoolConfig">
        <property name="maxIdle" value="50"/>
        <property name="maxTotal" value="100"/>
        <property name="maxWaitMillis" value="20000"/>
    </bean>

    <bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
        <property name="hostName" value="localhost"/>
        <property name="port" value="6379"/>
        <property name="poolConfig" ref="poolConfig"/>
    </bean>

    <bean id="jdkSeria" class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer"/>
    <bean id="stringSeria" class="org.springframework.data.redis.serializer.StringRedisSerializer"/>

    <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate">
        <property name="connectionFactory" ref="connectionFactory"/>
        <property name="keySerializer" ref="stringSeria"/>
        <property name="valueSerializer" ref="jdkSeria"/>
        <property name="defaultSerializer" ref="stringSeria"/>
    </bean>