1. redis缓存数据库

1.1. 为什么使用redis

传统的关系型数据库,如Mysql,已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件。缓存中间件的数据存放在内存中,能够做到更高效的读写。

目前市面上比较常用的缓存中间件有 RedisMemcached

1.2. redis缓存数据库的一致性问题

一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。

  • 强一致性:系统写入什么,读出来什么。用户体验最好,但实现起来往往对系统的性能影响大
  • 弱一致性:约束系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。
  • 最终一致性:最终一致性是弱一致性的一个特例,系统会保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型

1.3. 三个经典的缓存模式

1.3.1. 旁路缓存 (Cache-Aside Pattern)

旁路缓存遵从以下原则:

  1. 读的时候,先读缓存,缓存命中的话,直接返回缓存中的数据
  2. 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
  3. 更新的时候,先更新数据库,再删除缓存

1.3.2. 读写穿透 (Read-Through/Write through)

在该模式中,服务端把缓存作为主要数据存储。应用程序跟数据库缓存交互,都是通过抽象缓存层完成的。 其他原则和 旁路模式 相同。

所以,其实 读写穿透 只是在旁路模式上进行了一层封装,加上了一个抽象缓存层,它让代码更简介,同事也减少了数据源上的负载。

1.3.3. 异步缓存写入 (Write behind)

该模式整体的业务逻辑和 读写穿透 模式相同。只是读写穿透中,更新数据库是同步的,是实时的。而在 异步缓存写入 中的更新操作中,只更新缓存,不直接更新数据库。对数据库的额更新是通过 批量异步 的方式实现的。

在这种方式下,缓存和数据库的一致性不强,对一致性要求高的系统要谨慎使用。但是它适合频繁写的场景。

2. jedis实现缓存mysql

2.1. 配置文件

application.yml

#redis
redis:
  host: 192.168.50.131
  port: 6379
  timeout: 3
  password: 123456
  poolMaxTotal: 10
  poolMaxIdle: 10
  poolMaxWait: 3

pom.xml

<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

<!--以下为模块中使用到的依赖,可以选择性添加-->
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>
<!--注解依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

2.2. 其他文件

jedis配置:

/**
 * Redis配置bean
 * @author Mosfield
 */
@Data
@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfig {

    private String host;
    private int port;
    private int timeout;
    private String password;
    private int poolMaxTotal;
    private int poolMaxIdle;
    private int  poolMaxWait;
}

连接池工厂类

/**
 * jedis连接池工厂类
 * @author Mosfield
 */
@Service
@Slf4j
public class JedisPoolFactory {

    @Autowired
    RedisConfig redisConfig;

    @Bean
    public JedisPool JedisPoolFactory() {

        System.out.println(redisConfig.toString());
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
        poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
        poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
        JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout() * 1000, redisConfig.getPassword(), 0);
        return jedisPool;
    }

    /**
     * 测试redis连接是否正常。
     */
    public void testConnectRedis() {

        String host = redisConfig.getHost();
        int port = redisConfig.getPort();

        //连接本地的 Redis 服务
        Jedis jedis = new Jedis(host, port);
        //密码
        jedis.auth(redisConfig.getPassword());
        //查看服务是否运行
        try {
            jedis.ping();
            System.out.println("---------------redis预连接成功!---------------");
        } catch (Exception e) {
            log.error("Could not connect to Redis at " + host + ":" + port + " Connection refused!");
            throw new RuntimeException("redis连接异常!");
        }
    }
}

工具类

/**
 * 工具类
 * @Author Mosfield
 */
@Slf4j
public class RedisKeyUtils {
    /**
     * 根据类名,方法名和参数,生成用于缓存的MD5字符串
     *
     * @param className  类名
     * @param methodName 方法名
     * @param params     参数数组
     * @return
     */
    public static String getMd5Key(String className, String methodName, Object... params) {

        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(className);
        strBuilder.append(":");
        strBuilder.append(methodName);
        strBuilder.append(":");
        String paramsStr = UUID.randomUUID().toString();
        try {
            paramsStr = new ObjectMapper().writeValueAsString(params);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        strBuilder.append(paramsStr);
        String md5 = DigestUtils.md5DigestAsHex(strBuilder.toString().getBytes(StandardCharsets.UTF_8));
        log.info("info"+strBuilder);
        log.info("key"+md5);

        return md5;
    }
}

service类

/**
 * jedis核心业务类
 * @Author Mosfield
 */
@Service
@Slf4j
public class RedisService {

    @Autowired
    private JedisPool jedisPool;

    /**
     * 根据类和类型,返回缓存中对应的数据
     * @param key
     * @param clazz
     * @return
     * @param <T>
     */
    public <T> T get(String key, Class<T> clazz) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = jedis.get(key);
            if (str == null) {
                return null;
            }

            T t = null;
            try {
                t = new ObjectMapper().readValue(str, clazz);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return t;
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 根据key设置缓存
     * @param key
     * @param value
     * @return
     * @param <T>
     */
    public <T> boolean set(String key, T value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = null;
            try {
                str = new ObjectMapper().writeValueAsString(value);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            if (str == null || str.length() <= 0) {
                return false;
            }
            jedis.set(key, str);
            return true;
        } finally {
            returnToPool(jedis);
        }
    }

    private void returnToPool(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }

    }

    public boolean del(String key) {
        jedisPool.getResource().del(key);
        return true;
    }

    /**
     * 把key交给一个命名空间管理,以备更新的时候清除
     *
     * @param namespace
     * @param key
     */
    public void addKeyToCache(String namespace, String key) {
        ArrayList list = this.get(namespace, ArrayList.class);
        // 如果没有,则新建一条缓存
        if (list == null) {
            ArrayList<String> newList = new ArrayList<>();
            newList.add(key);
            this.set(namespace, newList);
            return;
        }

        list.add(key);
        this.set(namespace, list);

    }

    /**
     * 清除一个命名空间中的所有缓存
     *
     * @param namespace
     */
    public void delCacheByNamespace(String namespace) {
        Jedis jedis = jedisPool.getResource();
        // 获取该命名空间里的所有key
        ArrayList<String> list = this.get(namespace, ArrayList.class);
        if (list == null) {
            return;
        }
        String[] keys = list.toArray(new String[list.size()]);
        // 删除
        jedis.del(keys);
        // 释放连接
        returnToPool(jedis);
    }
}

2.3. 简单案例的思路和实现

我们使用 读写穿透 模式来实现一个简单的查询和删除案例,分别实现查询缓存和删除缓存的功能。

在该模块的编写中,我们的核心思路是,通过方法和参数,确定一个唯一的MD5值,以根据这个值来判断两次执行的方法是否一致。以这个MD5为key,以查询结果为值,存入redis。这样,当下次有同样的方法同样的参数的时候,就不查询数据库,而是直接返回缓存中的值。

当数据进行更新操作的时候,就删掉当前方法相关的所有值

另一个重要概念是,我们每次进行缓存写入的时候,其实将缓存人为的绑定到了一个命名空间。

这个命名空间本身也是一条缓存,key是命名空间的名字,value是一个列表,记录了该命名空间里所有缓存的key。这样,当我们执行数据库的删除操作时,就知道应该删除哪些缓存数据了。

Controller

/**
 * 品牌controller
 *
 * @Author Mosfield
 */
@RestController
public class BrandController {
    @Autowired
    BrandService brandService;

    /**
     * 根据id查询对应的品牌信息
     *
     * @param id 查找的信息的主键
     * @return
     */
    @GetMapping("/findById")
    public Result findById(@RequestParam("id") Integer id) {
        Brand brand = brandService.findById(id);
        return Result.success(brand);
    }

    /**
     * 添加
     *
     * @param brand 新增的信息
     * @return
     */
    @PostMapping("/addBrand")
    public Result addBrand(@RequestBody Brand brand) {
        boolean result = brandService.addBrand(brand);
        if (result) {
            return Result.success();
        }
        return Result.error("添加失败,请联系系统管理员");
    }

}

Service实现类(service接口省略)

/**
 * 品牌业务实现类
 * @Author Mosfield
 * @Date 2023-5-31
 */
@Service
public class BrandServiceImpl implements BrandService {

    @Autowired
    BrandCacheService cacheService;

    @Override
    public Brand findById(Integer id) {
        return cacheService.findById(id);
    }

    @Override
    public boolean addBrand(Brand brand) {
        int num = cacheService.addBrand(brand);
        if(num > 0){
            return true;
        }
        return false;
    }

}

缓存接口

/**
 * 缓存抽象层
 * 进dao层前,先查询缓存中是否存在
 * @Author Mosfield
 */
public interface BrandCacheService {
    /**
     * 根据id查询对应的品牌信息
     * @param id 查找的信息的主键
     * @return
     */
    Brand findById(Integer id);

    /**
     * 添加品牌
     * @param brand 新增的信息
     * @return
     */
    int addBrand(Brand brand);
}

缓存接口实现类

/**
 * 缓存抽象层
 *
 * @Author Mosfield
 * @Date 2023-5-31
 */
@Service
@Slf4j
public class BrandCacheServiceImpl implements BrandCacheService {
    /**
     * 当前类的名字
     */
    public static final String CLASS_NAME;

    static {
        CLASS_NAME = BrandCacheServiceImpl.class.getName();
    }

    @Autowired
    RedisService redisService;

    @Autowired
    BrandMapper brandMapper;

    @Override
    public Brand findById(Integer id) {
        // 计算这次请求的key
        String md5Key = RedisKeyUtils.getMd5Key(CLASS_NAME, "findById", id);
        // 根据请求,查看缓存中是否存在返回值
        Brand brand = redisService.get(md5Key, Brand.class);

        if (brand == null) {
            log.info("=============查询单条数据=============");
            // 查询数据库
            brand = brandMapper.findById(id);
            // 设置缓存
            redisService.set(md5Key, brand);
            // 把key放入命名空间
            redisService.addKeyToCache(CLASS_NAME, md5Key);
        }
        return brand;
    }

    @Override
    public int addBrand(Brand brand) {
        log.info("=============添加操作,清空缓存=============");
        // 更新数据库
        int result = brandMapper.addBrand(brand);
        // 清除当前命名空间的缓存
        redisService.delCacheByNamespace(CLASS_NAME);

        return result;
    }
}

dao层略,无变化

#

2. jedis实现缓存mysql

2.1. 配置文件

application.yml

#redis
redis:
  host: 192.168.50.131
  port: 6379
  timeout: 3
  password: 123456
  poolMaxTotal: 10
  poolMaxIdle: 10
  poolMaxWait: 3

pom.xml

<!--jedis-->
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

<!--以下为模块中使用到的依赖,可以选择性添加-->
<!--lombok-->
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <version>1.18.20</version>
</dependency>
<!--注解依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

2.2. 其他文件

jedis配置:

/**
 * Redis配置bean
 * @author Mosfield
 */
@Data
@Component
@ConfigurationProperties(prefix = "redis")
public class RedisConfig {

    private String host;
    private int port;
    private int timeout;
    private String password;
    private int poolMaxTotal;
    private int poolMaxIdle;
    private int  poolMaxWait;
}

连接池工厂类

/**
 * jedis连接池工厂类
 * @author Mosfield
 */
@Service
@Slf4j
public class JedisPoolFactory {

    @Autowired
    RedisConfig redisConfig;

    @Bean
    public JedisPool JedisPoolFactory() {

        System.out.println(redisConfig.toString());
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(redisConfig.getPoolMaxIdle());
        poolConfig.setMaxTotal(redisConfig.getPoolMaxTotal());
        poolConfig.setMaxWaitMillis(redisConfig.getPoolMaxWait() * 1000);
        JedisPool jedisPool = new JedisPool(poolConfig, redisConfig.getHost(), redisConfig.getPort(), redisConfig.getTimeout() * 1000, redisConfig.getPassword(), 0);
        return jedisPool;
    }

    /**
     * 测试redis连接是否正常。
     */
    public void testConnectRedis() {

        String host = redisConfig.getHost();
        int port = redisConfig.getPort();

        //连接本地的 Redis 服务
        Jedis jedis = new Jedis(host, port);
        //密码
        jedis.auth(redisConfig.getPassword());
        //查看服务是否运行
        try {
            jedis.ping();
            System.out.println("---------------redis预连接成功!---------------");
        } catch (Exception e) {
            log.error("Could not connect to Redis at " + host + ":" + port + " Connection refused!");
            throw new RuntimeException("redis连接异常!");
        }
    }
}

工具类

/**
 * 工具类
 * @Author Mosfield
 */
@Slf4j
public class RedisKeyUtils {
    /**
     * 根据类名,方法名和参数,生成用于缓存的MD5字符串
     *
     * @param className  类名
     * @param methodName 方法名
     * @param params     参数数组
     * @return
     */
    public static String getMd5Key(String className, String methodName, Object... params) {

        StringBuilder strBuilder = new StringBuilder();
        strBuilder.append(className);
        strBuilder.append(":");
        strBuilder.append(methodName);
        strBuilder.append(":");
        String paramsStr = UUID.randomUUID().toString();
        try {
            paramsStr = new ObjectMapper().writeValueAsString(params);
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        strBuilder.append(paramsStr);
        String md5 = DigestUtils.md5DigestAsHex(strBuilder.toString().getBytes(StandardCharsets.UTF_8));
        log.info("info"+strBuilder);
        log.info("key"+md5);

        return md5;
    }
}

service类

/**
 * jedis核心业务类
 * @Author Mosfield
 */
@Service
@Slf4j
public class RedisService {

    @Autowired
    private JedisPool jedisPool;

    /**
     * 根据类和类型,返回缓存中对应的数据
     * @param key
     * @param clazz
     * @return
     * @param <T>
     */
    public <T> T get(String key, Class<T> clazz) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = jedis.get(key);
            if (str == null) {
                return null;
            }

            T t = null;
            try {
                t = new ObjectMapper().readValue(str, clazz);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            return t;
        } finally {
            returnToPool(jedis);
        }
    }

    /**
     * 根据key设置缓存
     * @param key
     * @param value
     * @return
     * @param <T>
     */
    public <T> boolean set(String key, T value) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            String str = null;
            try {
                str = new ObjectMapper().writeValueAsString(value);
            } catch (JsonProcessingException e) {
                e.printStackTrace();
            }
            if (str == null || str.length() <= 0) {
                return false;
            }
            jedis.set(key, str);
            return true;
        } finally {
            returnToPool(jedis);
        }
    }

    private void returnToPool(Jedis jedis) {
        if (jedis != null) {
            jedis.close();
        }

    }

    public boolean del(String key) {
        jedisPool.getResource().del(key);
        return true;
    }

    /**
     * 把key交给一个命名空间管理,以备更新的时候清除
     *
     * @param namespace
     * @param key
     */
    public void addKeyToCache(String namespace, String key) {
        ArrayList list = this.get(namespace, ArrayList.class);
        // 如果没有,则新建一条缓存
        if (list == null) {
            ArrayList<String> newList = new ArrayList<>();
            newList.add(key);
            this.set(namespace, newList);
            return;
        }

        list.add(key);
        this.set(namespace, list);

    }

    /**
     * 清除一个命名空间中的所有缓存
     *
     * @param namespace
     */
    public void delCacheByNamespace(String namespace) {
        Jedis jedis = jedisPool.getResource();
        // 获取该命名空间里的所有key
        ArrayList<String> list = this.get(namespace, ArrayList.class);
        if (list == null) {
            return;
        }
        String[] keys = list.toArray(new String[list.size()]);
        // 删除对应的缓存
        jedis.del(keys);
        // 删除这个缓存本身
        jedis.del(namespace);
        // 释放连接
        returnToPool(jedis);
    }
}

2.3. 简单案例的思路和实现

我们使用 读写穿透 模式来实现一个简单的查询和删除案例,分别实现查询缓存和删除缓存的功能。

在该模块的编写中,我们的核心思路是,通过方法和参数,确定一个唯一的MD5值,以根据这个值来判断两次执行的方法是否一致。以这个MD5为key,以查询结果为值,存入redis。这样,当下次有同样的方法同样的参数的时候,就不查询数据库,而是直接返回缓存中的值。

当数据进行更新操作的时候,就删掉当前方法相关的所有值

另一个重要概念是,我们每次进行缓存写入的时候,其实将缓存人为的绑定到了一个命名空间。

这个命名空间本身也是一条缓存,key是命名空间的名字,value是一个列表,记录了该命名空间里所有缓存的key。这样,当我们执行数据库的删除操作时,就知道应该删除哪些缓存数据了。

Controller

/**
 * 品牌controller
 *
 * @Author Mosfield
 */
@RestController
public class BrandController {
    @Autowired
    BrandService brandService;

    /**
     * 根据id查询对应的品牌信息
     *
     * @param id 查找的信息的主键
     * @return
     */
    @GetMapping("/findById")
    public Result findById(@RequestParam("id") Integer id) {
        Brand brand = brandService.findById(id);
        return Result.success(brand);
    }

    /**
     * 添加
     *
     * @param brand 新增的信息
     * @return
     */
    @PostMapping("/addBrand")
    public Result addBrand(@RequestBody Brand brand) {
        boolean result = brandService.addBrand(brand);
        if (result) {
            return Result.success();
        }
        return Result.error("添加失败,请联系系统管理员");
    }

}

Service实现类(service接口省略)

/**
 * 品牌业务实现类
 * @Author Mosfield
 * @Date 2023-5-31
 */
@Service
public class BrandServiceImpl implements BrandService {

    @Autowired
    BrandCacheService cacheService;

    @Override
    public Brand findById(Integer id) {
        return cacheService.findById(id);
    }

    @Override
    public boolean addBrand(Brand brand) {
        int num = cacheService.addBrand(brand);
        if(num > 0){
            return true;
        }
        return false;
    }

}

缓存接口

/**
 * 缓存抽象层
 * 进dao层前,先查询缓存中是否存在
 * @Author Mosfield
 */
public interface BrandCacheService {
    /**
     * 根据id查询对应的品牌信息
     * @param id 查找的信息的主键
     * @return
     */
    Brand findById(Integer id);

    /**
     * 添加品牌
     * @param brand 新增的信息
     * @return
     */
    int addBrand(Brand brand);
}

缓存接口实现类

/**
 * 缓存抽象层
 *
 * @Author Mosfield
 * @Date 2023-5-31
 */
@Service
@Slf4j
public class BrandCacheServiceImpl implements BrandCacheService {
    /**
     * 当前类的名字
     */
    public static final String CLASS_NAME;

    static {
        CLASS_NAME = BrandCacheServiceImpl.class.getName();
    }

    @Autowired
    RedisService redisService;

    @Autowired
    BrandMapper brandMapper;

    @Override
    public Brand findById(Integer id) {
        // 计算这次请求的key
        String md5Key = RedisKeyUtils.getMd5Key(CLASS_NAME, "findById", id);
        // 根据请求,查看缓存中是否存在返回值
        Brand brand = redisService.get(md5Key, Brand.class);

        if (brand == null) {
            log.info("=============查询单条数据=============");
            // 查询数据库
            brand = brandMapper.findById(id);
            // 设置缓存
            redisService.set(md5Key, brand);
            // 把key放入命名空间
            redisService.addKeyToCache(CLASS_NAME, md5Key);
        }
        return brand;
    }

    @Override
    public int addBrand(Brand brand) {
        log.info("=============添加操作,清空缓存=============");
        // 更新数据库
        int result = brandMapper.addBrand(brand);
        // 清除当前命名空间的缓存
        redisService.delCacheByNamespace(CLASS_NAME);

        return result;
    }
}

dao层略,无变化

如人饮水,冷暖自知。
最后更新于 2023-08-02