1. redis缓存数据库
1.1. 为什么使用redis
传统的关系型数据库,如Mysql,已经不能适用所有的场景了,比如秒杀的库存扣减,APP首页的访问流量高峰等等,都很容易把数据库打崩,所以引入了缓存中间件。缓存中间件的数据存放在内存中,能够做到更高效的读写。
目前市面上比较常用的缓存中间件有 Redis 和 Memcached 。
1.2. redis缓存数据库的一致性问题
一致性就是数据保持一致,在分布式系统中,可以理解为多个节点中数据的值是一致的。
- 强一致性:系统写入什么,读出来什么。用户体验最好,但实现起来往往对系统的性能影响大
- 弱一致性:约束系统在写入成功后,不承诺立即可以读到写入的值,也不承诺多久之后数据能够达到一致,但会
尽可能地保证到某个时间级别(比如秒级别)后,数据能够达到一致状态。 - 最终一致性:最终一致性是弱一致性的一个特例,系统会
保证在一定时间内,能够达到一个数据一致的状态。这里之所以将最终一致性单独提出来,是因为它是弱一致性中非常推崇的一种一致性模型,也是业界在大型分布式系统的数据一致性上比较推崇的模型
1.3. 三个经典的缓存模式
1.3.1. 旁路缓存 (Cache-Aside Pattern)
旁路缓存遵从以下原则:
- 读的时候,先读缓存,缓存命中的话,直接返回缓存中的数据
- 缓存没有命中的话,就去读数据库,从数据库取出数据,放入缓存后,同时返回响应。
- 更新的时候,先更新数据库,再删除缓存
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层略,无变化



Comments NOTHING