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