前言
通过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>