目录

Life in Flow

Embrace your dreams and unlock your full potential at every stage of life.

X

SpringBoot整合Redis

Redis 远程连接配置

[root@localhost ~]# vim /etc/redis/6379.conf 
bind 0.0.0.0 
port 6379 
daemonize yes
protected-mode no
requirepass 123

SpringBoot 整合 Redis

引入依赖

spring-boot-starter-data-redis 包含了 jedis,封装了一类 SpringBoot 与 Redis 的连接工具。

<dependency
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
	</dependency>

编写 redisTemplate 的配置类,设置 redisConnectFactory

import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;

@Configuration
public class RedisConfig {
    @Bean
    public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

引入配置文件 application.properties

spring.redis.database=0
spring.redis.host=192.168.31.220
spring.redis.port=6379
spring.redis.password=

编写测试类

import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;

@RestController
public class RedisController {
    @Resource
    private RedisTemplate redisTemplate;

    @RequestMapping("/redis/setAndGet")
    @ResponseBody
    public String setAndGetValue(String name,String value){
	//使用Redis设置键值对
        redisTemplate.opsForValue().set(name,value);
	//从Redis中获取指定的键值对
        return (String) redisTemplate.opsForValue().get(name);
    }
}

测试结果

http://localhost:8080/redis/setAndGet?name=soulboy&value=123
123

封装 redisTemplate API

  • opsForValue 操作 String,Key,Value,包含过期 key,setBit 位操作等
  • opsForSet 操作 set
  • opsForHash 操作 hash
  • opsForZset 操作 SortSet
  • opsForList 操作 list 队列

封装 redisTemplate

package com.xdclass.mobile.xdclassmobileredis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Service;

import java.io.Serializable;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * Created by Administrator on 2018/10/6.
 */
@Service
public class RedisService {

    @Autowired
    private RedisTemplate redisTemplate;

    private static double size = Math.pow(2, 32);


    /**
     * 写入缓存
     *
     * @param key
     * @param offset   位 8Bit=1Byte
     * @return
     */
    public boolean setBit(String key, long offset, boolean isShow) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.setBit(key, offset, isShow);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存
     *
     * @param key
     * @param offset
     * @return
     */
    public boolean getBit(String key, long offset) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            result = operations.getBit(key, offset);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }


    /**
     * 写入缓存
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 写入缓存设置时效时间
     *
     * @param key
     * @param value
     * @return
     */
    public boolean set(final String key, Object value, Long expireTime) {
        boolean result = false;
        try {
            ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
            operations.set(key, value);
            redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
            result = true;
        } catch (Exception e) {
            e.printStackTrace();
        }
        return result;
    }

    /**
     * 批量删除对应的value
     *
     * @param keys
     */
    public void remove(final String... keys) {
        for (String key : keys) {
            remove(key);
        }
    }


    /**
     * 删除对应的value
     *
     * @param key
     */
    public void remove(final String key) {
        if (exists(key)) {
            redisTemplate.delete(key);
        }
    }

    /**
     * 判断缓存中是否有对应的value
     *
     * @param key
     * @return
     */
    public boolean exists(final String key) {
        return redisTemplate.hasKey(key);
    }

    /**
     * 读取缓存
     *
     * @param key
     * @return
     */
    public Object get(final String key) {
        Object result = null;
        ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
        result = operations.get(key);
        return result;
    }

    /**
     * 哈希 添加
     *
     * @param key
     * @param hashKey
     * @param value
     */
    public void hmSet(String key, Object hashKey, Object value) {
        HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
        hash.put(key, hashKey, value);
    }

    /**
     * 哈希获取数据
     *
     * @param key
     * @param hashKey
     * @return
     */
    public Object hmGet(String key, Object hashKey) {
        HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
        return hash.get(key, hashKey);
    }

    /**
     * 列表添加
     *
     * @param k
     * @param v
     */
    public void lPush(String k, Object v) {
        ListOperations<String, Object> list = redisTemplate.opsForList();
        list.rightPush(k, v);
    }

    /**
     * 列表获取
     *
     * @param k
     * @param l
     * @param l1
     * @return
     */
    public List<Object> lRange(String k, long l, long l1) {
        ListOperations<String, Object> list = redisTemplate.opsForList();
        return list.range(k, l, l1);
    }

    /**
     * 集合添加
     *
     * @param key
     * @param value
     */
    public void add(String key, Object value) {
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        set.add(key, value);
    }

    /**
     * 集合获取
     *
     * @param key
     * @return
     */
    public Set<Object> setMembers(String key) {
        SetOperations<String, Object> set = redisTemplate.opsForSet();
        return set.members(key);
    }

    /**
     * 有序集合添加
     *
     * @param key
     * @param value
     * @param scoure
     */
    public void zAdd(String key, Object value, double scoure) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        zset.add(key, value, scoure);
    }

    /**
     * 有序集合获取
     *
     * @param key
     * @param scoure
     * @param scoure1
     * @return
     */
    public Set<Object> rangeByScore(String key, double scoure, double scoure1) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        redisTemplate.opsForValue();
        return zset.rangeByScore(key, scoure, scoure1);
    }


    //第一次加载的时候将数据加载到redis中
    public void saveDataToRedis(String name) {
        double index = Math.abs(name.hashCode() % size);
        long indexLong = new Double(index).longValue();
        boolean availableUsers = setBit("availableUsers", indexLong, true);
    }

    //第一次加载的时候将数据加载到redis中
    public boolean getDataToRedis(String name) {

        double index = Math.abs(name.hashCode() % size);
        long indexLong = new Double(index).longValue();
        return getBit("availableUsers", indexLong);
    }

    /**
     * 有序集合获取排名
     *
     * @param key 集合名称
     * @param value 值
     */
    public Long zRank(String key, Object value) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        return zset.rank(key,value);
    }


    /**
     * 有序集合获取排名
     *
     * @param key
     */
    public Set<ZSetOperations.TypedTuple<Object>> zRankWithScore(String key, long start,long end) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        Set<ZSetOperations.TypedTuple<Object>> ret = zset.rangeWithScores(key,start,end);
        return ret;
    }

    /**
     * 有序集合添加
     *
     * @param key
     * @param value
     */
    public Double zSetScore(String key, Object value) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        return zset.score(key,value);
    }


    /**
     * 有序集合添加分数
     *
     * @param key
     * @param value
     * @param scoure
     */
    public void incrementScore(String key, Object value, double scoure) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        zset.incrementScore(key, value, scoure);
    }


    /**
     * 有序集合获取排名
     *
     * @param key
     */
    public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithScore(String key, long start,long end) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeByScoreWithScores(key,start,end);
        return ret;
    }

    /**
     * 有序集合获取排名
     *
     * @param key
     */
    public Set<ZSetOperations.TypedTuple<Object>> reverseZRankWithRank(String key, long start, long end) {
        ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
        Set<ZSetOperations.TypedTuple<Object>> ret = zset.reverseRangeWithScores(key, start, end);
        return ret;
    }

}

测试 redisTemplate 工具类

@RestController
public class RedisController {
    @Resource
    private RedisService service;

    @RequestMapping("/redis/setAndGet1")
    @ResponseBody
    public String setAndGetValueV2(String name,String value){
        service.set(name,value);
        return service.get(name).toString();
    }
}

SpringBoot 整合 MyBatis

引入依赖

<dependency>
     <groupId>mysql</groupId>
     <artifactId>mysql-connector-java</artifactId>
  </dependency>
  <dependency>
      <groupId>org.mybatis.spring.boot</groupId>
      <artifactId>mybatis-spring-boot-starter</artifactId>
      <version>1.3.0</version>
  </dependency>
  <dependency>
    <groupId>org.mybatis.generator</groupId>
    <artifactId>mybatis-generator-core</artifactId>
    <scope>test</scope>
    <version>1.3.2</version>
    <optional>true</optional>
  </dependency>
  <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-jdbc</artifactId>
  </dependency>
  <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.10</version>
  </dependency>
  <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>1.2.7</version>
  </dependency>

引入配置文件 application.yml

这里采用 Druid 数据库连接池。

spring:
   datasource:
      url: jdbc:mysql://192.168.31.220:33306/test?useUnicode=true&characterEncoding=UTF-8&zeroDateTimeBehavior=convertToNull&autoReconnect=true&connectTimeout=3000&socketTimeout=1000
      username: root         
      password: 123       
      type: com.alibaba.druid.pool.DruidDataSource
      driver-class-name: com.mysql.jdbc.Driver
      minIdle: 5
      maxActive: 100
      initialSize: 10
      maxWait: 60000
      timeBetweenEvictionRunsMillis: 60000
      minEvictableIdleTimeMillis: 300000
      validationQuery: select 'x'
      testWhileIdle: true
      testOnBorrow: false
      testOnReturn: false
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 50
      removeAbandoned: true
      filters: stat # ,wall,log4j # 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
      connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
      useGlobalDataSourceStat: true # 合并多个DruidDataSource的监控数据
      druidLoginName: wjf # 登录druid的账号
      druidPassword: wjf # 登录druid的密码
      cachePrepStmts: true  # 开启二级缓存
mybatis:
   typeAliasesPackage: com.soulboy.mobile.soulboymobileredis.mapper      # 需要学员自己修改填写
   mapperLocations: classpath:/com/soulboy/mobile/soulboymobileredis/mapper/*.xml
   mapperScanPackage: com.soulboy.mobile.soulboymobileredis.mapper
   configLocation: classpath:/mybatis-config.xml

引入 mybatis-config.xml

<?xml version="1.0" encoding="UTF-8"?>  
<!DOCTYPE configuration  
     PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  
     "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<settings>
	    <!-- 使全局的映射器启用或禁用缓存。 -->
		<setting name="cacheEnabled" value="true" />
		<!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 -->
		<setting name="lazyLoadingEnabled" value="true" />
		<!-- 当启用时,有延迟加载属性的对象在被调用时将会完全加载任意属性。否则,每种属性将会按需要加载。 -->      
         <setting name="aggressiveLazyLoading" value="true"/>      
         <!-- 是否允许单条sql 返回多个数据集  (取决于驱动的兼容性) default:true -->
		<setting name="multipleResultSetsEnabled" value="true" />
		<!-- 是否可以使用列的别名 (取决于驱动的兼容性) default:true -->
		<setting name="useColumnLabel" value="true" />
		<!-- 允许JDBC 生成主键。需要驱动器支持。如果设为了true,这个设置将强制使用被生成的主键,有一些驱动器不兼容不过仍然可以执行。  default:false  -->
		<setting name="useGeneratedKeys" value="false" />
		<!-- 指定 MyBatis 如何自动映射 数据基表的列 NONE:不隐射 PARTIAL:部分  FULL:全部  -->
		<setting name="autoMappingBehavior" value="PARTIAL" />
		<!-- 这是默认的执行类型  (SIMPLE: 简单; REUSE: 执行器可能重复使用prepared statements语句;BATCH: 执行器可以重复执行语句和批量更新)  -->
		<setting name="defaultExecutorType" value="SIMPLE" />
	
		<setting name="defaultStatementTimeout" value="25" />
	
		<setting name="defaultFetchSize" value="100" />
	
		<setting name="safeRowBoundsEnabled" value="false" />
		<!-- 使用驼峰命名法转换字段。 -->
		<setting name="mapUnderscoreToCamelCase" value="true" />
		<!-- 设置本地缓存范围 session:就会有数据的共享  statement:语句范围 (这样就不会有数据的共享 ) defalut:session -->
		<setting name="localCacheScope" value="SESSION" />
		<!-- 默认为OTHER,为了解决oracle插入null报错的问题要设置为NULL -->
		<setting name="jdbcTypeForNull" value="NULL" />
		<setting name="lazyLoadTriggerMethods" value="equals,clone,hashCode,toString" />
	</settings>

</configuration>

引入 DataSourceConfig 配置

import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
@MapperScan("com.xdclass.mobile.xdclassmobileredis.mapper")
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 50)
public class DataSourceConfig {
    private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.type}")
    private String dbType;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Value("${spring.datasource.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;

    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;

    @Value("${spring.datasource.filters}")
    private String filters;

    @Value("${spring.datasource.connectionProperties}")
    private String connectionProperties;

    @Value("${spring.datasource.useGlobalDataSourceStat}")
    private boolean useGlobalDataSourceStat;

    @Value("${spring.datasource.druidLoginName}")
    private String druidLoginName;

    @Value("${spring.datasource.druidPassword}")
    private String druidPassword;

    @Bean(name="dataSource",destroyMethod = "close", initMethod="init")
    @Primary //不要漏了这
    public DataSource dataSource(){
        DruidDataSource datasource = new DruidDataSource();
        try {
            datasource.setUrl(this.dbUrl);
            datasource.setDbType(dbType);
            datasource.setUsername(username);
            datasource.setPassword(password);
            datasource.setDriverClassName(driverClassName);
            datasource.setInitialSize(initialSize);
            datasource.setMinIdle(minIdle);
            datasource.setMaxActive(maxActive);
            datasource.setMaxWait(maxWait);
            datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
            datasource.setValidationQuery(validationQuery);
            datasource.setTestWhileIdle(testWhileIdle);
            datasource.setTestOnBorrow(testOnBorrow);
            datasource.setTestOnReturn(testOnReturn);
            datasource.setPoolPreparedStatements(poolPreparedStatements);
            datasource.setFilters(filters);
        } catch (SQLException e) {
            logger.error("druid configuration initialization filter", e);
        }
        return datasource;
    }

    /////////  下面是druid 监控访问的设置  /////////////////
    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean reg = new ServletRegistrationBean();
        reg.setServlet(new StatViewServlet());
        reg.addUrlMappings("/druid/*");  //url 匹配
        reg.addInitParameter("allow", "192.168.16.110,127.0.0.1"); // IP白名单 (没有配置或者为空,则允许所有访问)
        reg.addInitParameter("deny", "192.168.16.111"); //IP黑名单 (存在共同时,deny优先于allow)
        reg.addInitParameter("loginUsername", this.druidLoginName);//登录名
        reg.addInitParameter("loginPassword", this.druidPassword);//登录密码
        reg.addInitParameter("resetEnable", "false"); // 禁用HTML页面上的“Reset All”功能
        return reg;
    }

    @Bean(name="druidWebStatFilter")
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); //忽略资源
        filterRegistrationBean.addInitParameter("profileEnable", "true");
        filterRegistrationBean.addInitParameter("principalCookieName", "USER_COOKIE");
        filterRegistrationBean.addInitParameter("principalSessionName", "USER_SESSION");
        return filterRegistrationBean;
    }
}

编写 Mapper 类:注解方式

@Mapper
@Component

public interface UserMapper {

	@Insert("insert sys_user(id,user_name) values(#{id},#{userName})")
	void insert(User u);

	@Update("update sys_user set user_name = #{userName} where id=#{id} ")
	void update(User u);

	@Delete("delete from sys_user where id=#{id} ")
	void delete(@Param("id") String id);

	@Select("select id,user_name from sys_user where id=#{id} ")
	User find(@Param("id") String id);

	//注:方法名和要UserMapper.xml中的id一致
	List<User> query(@Param("userName") String userName);

	@Delete("delete from sys_user")
	void deleteAll();
}

编写 Mapper 类:XML 配置方式


测试类

@RestController
public class UserController {
    @Resource
    private UserMapper userMapper;

    @RequestMapping("/getUser")
    @ResponseBody
    public User getUser(String id) {
        User user = userMapper.find(id);
        return user;
    }
}

测试

// http://localhost:8080/getUser?id=1
{
  "id": "1",
  "userName": "soulboy"
}

MyBatis 整合 Redis(缓存)

 请求第一次访问从 TRDB 中获取数据,并更新至 Redis,第二次访问时直接从 Redis 中获取数据。

@RestController
public class UserController {
    private static final String key = "userCache_";

    @Resource
    private UserMapper userMapper;

    @Resource
    private RedisService redisService;

    /**
     * set值和get值的时候序列化方式必须保持一致
     * @param id
     * @return
     */
    @RequestMapping("/getUserCache")
    @ResponseBody
    public User getUseCache(String id) {

        //step1 先从redis里面取值
        User user =  (User)redisService.get(key + id);
//默认使用JDK序列化方式

        //step2 如果拿不到则从DB取值
        if (user == null) {
            User userDB = userMapper.find(id);
            System.out.println("fresh value from DB id:" + id);

            //step3 DB非空情况刷新redis值
            if (userDB != null) {
                redisService.set(key + id, userDB);
//默认使用JDK序列化方式
                return userDB;
            }
        }
        return user;
    }

}

测试

// http://localhost:8080/getUserCache?id=1
{
  "id": "1",
  "userName": "soulboy"
}

Redis Desktop Manager 查看数据
mybatis整合redis作为缓存

SpringBoot cache

 上述示例中的 step1、step2、step3 包含大量的重复逻辑,为了消除代码的容易,专业于业务代码,可以使用 SpringBoot cache 注解。SpringBoot cache 可以结合 Redis、ehcache 等缓存。一级缓存是:sqlSession,SQL 建立连接到关闭连接的数据缓存; 二级缓存是:全局。

  • @CacheConfig(cacheNames="userInfoCache") 在同个 Redis 里面必须唯一
  • @Cacheable(查) : 来划分可缓存的方法 - 即,结果存储在缓存中的方法,以便在后续调用(具有相同的参数)时,返回缓存中的值而不必实际执行该方法
  • @CachePut(修改、增加) : 当需要更新缓存而不干扰方法执行时,可以使用@CachePut 注释。也就是说,始终执行该方法并将其结果放入缓存中(根据@CachePut 选项)
  • @CacheEvict(删除) : 对于从缓存中删除陈旧或未使用的数据非常有用,指示缓存范围内的驱逐是否需要执行而不仅仅是一个条目驱逐

引入 Maven 依赖

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-cache</artifactId>
		</dependency>

开启缓存注解: @EnableCaching

@Configuration
@EnableCaching
public class RedisConfig {
    @Bean
    public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

在 Service 层的方法上面加入 SpEL

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

@Service
@CacheConfig(cacheNames="userInfoCache") // 本类内方法指定使用缓存时,默认的名称就是userInfoCache
@Transactional(propagation=Propagation.REQUIRED,readOnly=false,rollbackFor=Exception.class)
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 因为必须要有返回值,才能保存到数据库中,如果保存的对象的某些字段是需要数据库生成的,
    //那保存对象进数据库的时候,就没必要放到缓存了
    @CachePut(key="#p0.id")  //#p0表示第一个参数
    //必须要有返回值,否则没数据放到缓存中
    public User insertUser(User u){
        this.userMapper.insert(u);
        //u对象中可能只有只几个有效字段,其他字段值靠数据库生成,比如id
        return this.userMapper.find(u.getId());
    }

    @CachePut(key="#p0.id")
    public User updateUser(User u){
        this.userMapper.update(u);
        //可能只是更新某几个字段而已,所以查次数据库把数据全部拿出来全部
        return this.userMapper.find(u.getId());
    }

    @Nullable
    @Cacheable(key="#p0") // @Cacheable 会先查询缓存,如果缓存中存在,则不执行方法
;#p0代表第一个参数
    public User findById(String id){
        System.err.println("根据id=" + id +"获取用户对象,从数据库中获取");
        Assert.notNull(id,"id不用为空");
        return this.userMapper.find(id);
    }

    @CacheEvict(key="#p0")  //删除缓存名称为userInfoCache,key等于指定的id对应的缓存
    public void deleteById(String id){
        this.userMapper.delete(id);
    }

    //清空缓存名称为userInfoCache(看类名上的注解)下的所有缓存
    //如果数据失败了,缓存时不会清除的
    @CacheEvict(allEntries = true)
    public void deleteAll(){
        this.userMapper.deleteAll();
    }

    @Nullable
    @Cacheable(value = "UserInfoList", keyGenerator = "simpleKeyGenerator") // @Cacheable 会先查询缓存,如果缓存中存在,则不执行方法
    public User findByIdTtl(String id){
        System.err.println("根据id=" + id +"获取用户对象,从数据库中获取");
        Assert.notNull(id,"id不用为空");
        return this.userMapper.find(id);
    }
}

编写 Controller 层测试代码

@RestController
public class UserController {

    @Resource
    private UserService userService;

    @RequestMapping("/getByCache")
    @ResponseBody
    public User getByCache(String id) {
        User user = userService.findById(id);
        return user;
    }

    @ResponseBody
    @RequestMapping(value = "/getexpire", method = RequestMethod.GET)
    public User findByIdTtl(String id) {
        User u = new User();
        try{
            u = userService.findByIdTtl(id);
        }catch (Exception e){
            System.err.println(e.getMessage());
        }
        return u;
    }
}

测试结果

// http://localhost:8080/getByCache?id=1
{
  "id": "1",
  "userName": "soulboy"
}
根据id=1获取用户对象,从数据库中获取
再次访问// http://localhost:8080/getByCache?id=1
	发现控制台没有打印,此时证明数据是从redis缓存中获取。

SpringBoot cache 存在的问题

生成 key 过于简单
 生成 key 过于简单,userCache::1 容易冲突。
无法设置过期时间,默认过期时间为永久不过期
 默认过期时间是 -1 ,代表永不过期。
配置序列化方式,默认的是序列化 JDKSerialazable
 JDKSerialazable 序列化方式对空值、日期的支持不好。

修改 RedisConfig 文件

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;

import javax.annotation.Resource;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
@EnableCaching
public class RedisConfig {

    @Bean
    public RedisTemplate<String,String> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    /**
     * 生成key过于简单,userCache::1容易冲突。
     * @return
     */
    @Bean
    public KeyGenerator simpleKeyGenerator() {
        return (o, method, objects) -> {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append(o.getClass().getSimpleName());
            stringBuilder.append(".");
            stringBuilder.append(method.getName());
            stringBuilder.append("[");
            for (Object obj : objects) {
                stringBuilder.append(obj.toString());
            }
            stringBuilder.append("]");
            return stringBuilder.toString();
        };
    }

    /**
     * 解决:默认过期时间为永久不过期的问题
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        return new RedisCacheManager(
                RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory),
                this.getRedisCacheConfigurationWithTtl(600), // 默认策略,未配置的 key 会使用这个。
                this.getRedisCacheConfigurationMap() // 指定 key 策略
        );
    }
    //针对指定的key设置缓存时间,其他未配置的key则使用默认策略。
    private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
        Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
        redisCacheConfigurationMap.put("UserInfoList", this.getRedisCacheConfigurationWithTtl(100));
        redisCacheConfigurationMap.put("UserInfoListAnother", this.getRedisCacheConfigurationWithTtl(18000));

        return redisCacheConfigurationMap;
    }

    /**
     * 修改默认序列化方式
     * @param seconds
     * @return
     */
    private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer seconds) {
        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper om = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
        redisCacheConfiguration = redisCacheConfiguration.serializeValuesWith(
                RedisSerializationContext
                        .SerializationPair
                        .fromSerializer(jackson2JsonRedisSerializer)//添加jackson的序列化方式
        ).entryTtl(Duration.ofSeconds(seconds));

        return redisCacheConfiguration;
    }
}

Service 层代码

import com.xdclass.mobile.xdclassmobileredis.domain.User;
import com.xdclass.mobile.xdclassmobileredis.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

@Service
@CacheConfig(cacheNames="userInfoCache") // 本类内方法指定使用缓存时,默认的名称就是userInfoCache
@Transactional(propagation=Propagation.REQUIRED,readOnly=false,rollbackFor=Exception.class)
public class UserService {

    @Autowired
    private UserMapper userMapper;

    // 因为必须要有返回值,才能保存到数据库中,如果保存的对象的某些字段是需要数据库生成的,
    //那保存对象进数据库的时候,就没必要放到缓存了
    @CachePut(key="#p0.id")  //#p0表示第一个参数
    //必须要有返回值,否则没数据放到缓存中
    public User insertUser(User u){
        this.userMapper.insert(u);
        //u对象中可能只有只几个有效字段,其他字段值靠数据库生成,比如id
        return this.userMapper.find(u.getId());
    }

    @CachePut(key="#p0.id")
    public User updateUser(User u){
        this.userMapper.update(u);
        //可能只是更新某几个字段而已,所以查次数据库把数据全部拿出来全部
        return this.userMapper.find(u.getId());
    }

    @Nullable
    //@Cacheable(key="#p0") // @Cacheable 会先查询缓存,如果缓存中存在,则不执行方法 #p0代表第一个参数
    @Cacheable(keyGenerator = "simpleKeyGenerator")//使用自定义key生成策略,防止key的冲突
    //key的格式:userInfoCache::UserService.findById[1]
    public User findById(String id){
        System.err.println("根据id=" + id +"获取用户对象,从数据库中获取");
        Assert.notNull(id,"id不用为空");
        return this.userMapper.find(id);
    }

    @CacheEvict(key="#p0")  //删除缓存名称为userInfoCache,key等于指定的id对应的缓存
    public void deleteById(String id){
        this.userMapper.delete(id);
    }

    //清空缓存名称为userInfoCache(看类名上的注解)下的所有缓存
    //如果数据失败了,缓存时不会清除的
    @CacheEvict(allEntries = true)
    public void deleteAll(){
        this.userMapper.deleteAll();
    }

    @Nullable
    // @Cacheable 会先查询缓存,如果缓存中存在,则不执行方法
    //使用自定义key生成策略,防止key的冲突
    @Cacheable(value = "UserInfoList", keyGenerator = "simpleKeyGenerator") 
    //key的格式:UserInfoList::UserService.findByIdTtl[1]
    public User findByIdTtl(String id){
        System.err.println("根据id=" + id +"获取用户对象,从数据库中获取");
        Assert.notNull(id,"id不用为空");
        return this.userMapper.find(id);
    }
}

Controller 层代码

@RestController
public class UserController {
    @Resource
    private UserService userService;

    @RequestMapping("/getByCache")
    @ResponseBody
    public User getByCache(String id) {
        User user = userService.findById(id);
        return user;
    }

    @ResponseBody
    @RequestMapping(value = "/getexpire", method = RequestMethod.GET)
    public User findByIdTtl(String id) {
        User u = new User();
        try{
            u = userService.findByIdTtl(id);
        }catch (Exception e){
            System.err.println(e.getMessage());
        }
        return u;
    }
}

测试

// http://localhost:8080/getexpire?id=1
{
  "id": "1",
  "userName": "soulboy"
}

ttl

压测 Redis cache VS TRDB


ab 是 Apache HTTP server benchmarking tool,可以用以测试 HTTP 请求的服务器性能

# 安装ab测试工具
[root@localhost ~]# yum install -y httpd-tools
[root@localhost ~]# ab -V
This is ApacheBench, Version 2.3 <$Revision: 1430300 $>

# ab测试工具命令参数含义
-n	#进行http请求的总个数
-c	#同时并发请求的Client数量,即请求的并发数
qps	#qps即每秒并发数,request per second,通过统计qps来测试性能。

# 进行测试
# Redis cache
ab -n1000 -c10 http://192.168.31.230:8080/getByCache?id=1
# TRDB
ab -n1000 -c10 http://192.168.31.230:8080/getUser?id=1

#  1000请求数   10并发
Mysql:		1347.98 [#/sec] (mean)
Redis cache:	1713.87 [#/sec] (mean)

#  1000请求数   100并发
Mysql:		2510.13 [#/sec] (mean)
Redis cache:	4440.42 [#/sec] (mean)

#  10000请求数   100并发
Mysql:		2953.38 [#/sec] (mean)
Redis cache:	5752.92 [#/sec] (mean)

#  10000请求数   300并发
Mysql:		apr_socket_recv: Connection refused(服务过载,拒绝连接)
Redis cache:	6241.05 [#/sec] (mean)

#  10000请求数   800并发
Mysql:		apr_socket_recv: Connection refused(服务过载,拒绝连接)
Redis cache:	4831.11 [#/sec] (mean)

Redis 实现分布式集群环境下的 session 共享

 机器部署同一套服务(代码),性能更好,更承受更高的用户并发,集群之间可以通过 Redis 来共享 session。

  • Cookie: Cookie 是一小段文本信息,伴随着用户请求和页面在 Web 服务器和浏览器之间传递。Cookie 包含每次用户访问站点时 Web 应用程序都可以读取的信息,我们可以看到在服务器写的 cookie,会通过响应头 Set-Cookie 的方式写入到浏览器。
  • Session:HTTP 协议是无状态的,并非 TCP 一样进行三次握手,对于一个浏览器发出的多次请求,Web 服务器无法区分是不是来源于同一个浏览器。所以服务器为了区分这个过程会通过一个 sessionid 来区分请求,而 sessionid 保存在 cookie 中。引入 Maven 依赖
<dependency>
			<groupId>org.springframework.session</groupId>
			<artifactId>spring-session-data-redis</artifactId>
		</dependency>

开启 Redis session 缓存:@EnableRedisHttpSession
 DataSourceConfig

  • maxInactiveIntervalInSeconds 指定缓存的时间
import com.alibaba.druid.pool.DruidDataSource;
import com.alibaba.druid.support.http.StatViewServlet;
import com.alibaba.druid.support.http.WebStatFilter;
import org.mybatis.spring.annotation.MapperScan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import javax.sql.DataSource;
import java.sql.SQLException;

@Configuration
@MapperScan("com.xdclass.mobile.xdclassmobileredis.mapper")
@EnableRedisHttpSession(maxInactiveIntervalInSeconds= 50)
public class DataSourceConfig {
    private Logger logger = LoggerFactory.getLogger(DataSourceConfig.class);

    @Value("${spring.datasource.url}")
    private String dbUrl;

    @Value("${spring.datasource.type}")
    private String dbType;

    @Value("${spring.datasource.username}")
    private String username;

    @Value("${spring.datasource.password}")
    private String password;

    @Value("${spring.datasource.driver-class-name}")
    private String driverClassName;

    @Value("${spring.datasource.initialSize}")
    private int initialSize;

    @Value("${spring.datasource.minIdle}")
    private int minIdle;

    @Value("${spring.datasource.maxActive}")
    private int maxActive;

    @Value("${spring.datasource.maxWait}")
    private int maxWait;

    @Value("${spring.datasource.timeBetweenEvictionRunsMillis}")
    private int timeBetweenEvictionRunsMillis;

    @Value("${spring.datasource.minEvictableIdleTimeMillis}")
    private int minEvictableIdleTimeMillis;

    @Value("${spring.datasource.validationQuery}")
    private String validationQuery;

    @Value("${spring.datasource.testWhileIdle}")
    private boolean testWhileIdle;

    @Value("${spring.datasource.testOnBorrow}")
    private boolean testOnBorrow;

    @Value("${spring.datasource.testOnReturn}")
    private boolean testOnReturn;

    @Value("${spring.datasource.poolPreparedStatements}")
    private boolean poolPreparedStatements;

    @Value("${spring.datasource.filters}")
    private String filters;

    @Value("${spring.datasource.connectionProperties}")
    private String connectionProperties;

    @Value("${spring.datasource.useGlobalDataSourceStat}")
    private boolean useGlobalDataSourceStat;

    @Value("${spring.datasource.druidLoginName}")
    private String druidLoginName;

    @Value("${spring.datasource.druidPassword}")
    private String druidPassword;

    @Bean(name="dataSource",destroyMethod = "close", initMethod="init")
    @Primary //不要漏了这
    public DataSource dataSource(){
        DruidDataSource datasource = new DruidDataSource();
        try {
            datasource.setUrl(this.dbUrl);
            datasource.setDbType(dbType);
            datasource.setUsername(username);
            datasource.setPassword(password);
            datasource.setDriverClassName(driverClassName);
            datasource.setInitialSize(initialSize);
            datasource.setMinIdle(minIdle);
            datasource.setMaxActive(maxActive);
            datasource.setMaxWait(maxWait);
            datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
            datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
            datasource.setValidationQuery(validationQuery);
            datasource.setTestWhileIdle(testWhileIdle);
            datasource.setTestOnBorrow(testOnBorrow);
            datasource.setTestOnReturn(testOnReturn);
            datasource.setPoolPreparedStatements(poolPreparedStatements);
            datasource.setFilters(filters);
        } catch (SQLException e) {
            logger.error("druid configuration initialization filter", e);
        }
        return datasource;
    }

    /////////  下面是druid 监控访问的设置  /////////////////
    @Bean
    public ServletRegistrationBean druidServlet() {
        ServletRegistrationBean reg = new ServletRegistrationBean();
        reg.setServlet(new StatViewServlet());
        reg.addUrlMappings("/druid/*");  //url 匹配
        reg.addInitParameter("allow", "192.168.16.110,127.0.0.1"); // IP白名单 (没有配置或者为空,则允许所有访问)
        reg.addInitParameter("deny", "192.168.16.111"); //IP黑名单 (存在共同时,deny优先于allow)
        reg.addInitParameter("loginUsername", this.druidLoginName);//登录名
        reg.addInitParameter("loginPassword", this.druidPassword);//登录密码
        reg.addInitParameter("resetEnable", "false"); // 禁用HTML页面上的“Reset All”功能
        return reg;
    }

    @Bean(name="druidWebStatFilter")
    public FilterRegistrationBean filterRegistrationBean() {
        FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
        filterRegistrationBean.setFilter(new WebStatFilter());
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); //忽略资源
        filterRegistrationBean.addInitParameter("profileEnable", "true");
        filterRegistrationBean.addInitParameter("principalCookieName", "USER_COOKIE");
        filterRegistrationBean.addInitParameter("principalSessionName", "USER_SESSION");
        return filterRegistrationBean;
    }
}

Controller 层代码

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@RestController
public class SessionController {

    @RequestMapping(value = "/setSession", method = RequestMethod.GET)
    public Map<String, Object> setSession (HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        request.getSession().setAttribute("request Url", request.getRequestURL());
        map.put("request Url", request.getRequestURL());
        return map;
    }

    @RequestMapping(value = "/getSession", method = RequestMethod.GET)
    public Object getSession (HttpServletRequest request){
        Map<String, Object> map = new HashMap<>();
        map.put("sessionIdUrl",request.getSession().getAttribute("request Url"));
 //设置sessionIdUrl
        map.put("sessionId", request.getSession().getId());
 //设置sessionId
        return map;
    }
}

测试

// http://192.168.31.230:8080/setSession
{
  "request Url": "http://192.168.31.230:8080/setSession"
}

// http://192.168.31.230:8080/getSession
{
  "sessionIdUrl": "http://192.168.31.230:8080/setSession",
  "sessionId": "2c950216-dbd1-4b3b-a748-67e71611a084"
}

查询 Redis

spring:session:sessions:expires:81d06c82-5312-41d7-8afe-4d08b77352d6

session共享


作者:Soulboy