1.热点数据的缓存
把经常被访问的数据存储到redis中,以后再查找该数据时,优先从redis中查询,如果redis没有被命中,则才会查询数据。并把查询的结果放入redis中以便下次能从redis中获取。
什么样的数据适合放入缓存中?
1. 查询频率高的数据
2. 修改频率低的数据
3. 数据安全性要求不高的
缓存的作用:
1. 提高查询效率
2. 降低数据库的访问频率,减少数据库的压力
如何使用redis作为缓存
1.依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
2.配置
#数据源
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/qy168?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#redis单机版
spring.redis.host=192.168.223.158
spring.redis.port=6379
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
3.Dao层
public interface DeptDao extends BaseMapper<Dept> {
}
4.业务层
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Dept findById(Integer id) {
ValueOperations valueOperations = redisTemplate.opsForValue();
//1.查询缓存
Object o = valueOperations.get("dept::" + id);
//2.判断是否缓存命中
if(o!=null&&o instanceof Dept){//o instanceof Dept: 判断对象是否属于Dept类型
return (Dept) o;
}
//3. 缓存没有命中则查询数据库
Dept dept = deptDao.selectById(id);
if(dept!=null){
//4. 查询的数据存入缓存中以便下次能从缓存中获取
valueOperations.set("dept::" + id,dept);
}
return dept;
}
@Override
@Transactional
public int delete(Integer id) {
int row = deptDao.deleteById(id);
redisTemplate.delete("dept::"+id);
return row;
}
@Override
public Dept insert(Dept dept) {
int insert = deptDao.insert(dept);
return dept;
}
@Override
public Dept update(Dept dept) {
redisTemplate.opsForValue().set("dept::"+dept.getDid(),dept);
int i = deptDao.updateById(dept);
return dept;
}
}
4.控制层
@RestController
@RequestMapping("/dept")
public class DeptController {
@Autowired
private DeptService deptService;
@GetMapping("/getById/{id}")
public Dept getById(@PathVariable Integer id ){
Dept dept = deptService.findById(id);
return dept;
}
@GetMapping("/delete/{id}")
public int delete(@PathVariable Integer id ){
return deptService.delete(id);
}
@GetMapping("/insert")
public Dept insert(Dept dept){ //接受表单参数和查询参数。
return deptService.insert(dept);
}
@GetMapping("/update")
public Dept update(Dept dept ){
return deptService.update(dept);
}
}
然后启动项目访问查询,第一次会查询数据库,可以在控制台打印查询数据库结果,之后便不会再查询数据库。
我们再使用redis作为缓存时,每次都需要自己添加缓存代码【非业务代码】。未来维护时还要维护redis非业务代码。
spring框架也会想到使用AOP解决业务代码和缓存的非业务代码的重合。--使用了缓存的注解。
如何使用spring缓存注解的方式实现redis缓存功能。
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer 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);
// 配置序列化(解决乱码的问题),过期时间600秒
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(600)) //缓存过期10分钟 ---- 业务需求。
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))//设置key的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) //设置value的序列化
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}
然后修改业务层代码
package com.ykq.service;
import com.ykq.dao.DeptDao;
import com.ykq.entity.Dept;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* @program: qy168-springboot-redis-02
* @description:
* @author: 闫克起2
* @create: 2023-08-31 14:34
**/
@Service
public class DeptServiceImpl implements DeptService {
@Autowired
private DeptDao deptDao;
@Autowired
private RedisTemplate redisTemplate;
//该注解用于查询功能方法上。 先查询缓存中是否存在cacheNames+"::"+key是否存在,
// 如果存在则不执行该方法,如果不存在则执行该方法并把该方法的返回结果存入缓存中。以cacheName+"::"+key作为缓存的key
@Cacheable(cacheNames = "dept",key = "#id")
public Dept findById(Integer id) {
//3. 缓存没有命中则查询数据库
Dept dept = deptDao.selectById(id);
return dept;
}
//把缓存中的数据进行移除,以cacheName+"::"+key作为缓存的key移除
@CacheEvict(cacheNames = "dept",key = "#id")
@Transactional
public int delete(Integer id) {
int row = deptDao.deleteById(id);
return row;
}
@Override
public Dept insert(Dept dept) {
int insert = deptDao.insert(dept);
return dept;
}
//把该方法的返回结果重新写入到缓存中
@CachePut(cacheNames = "dept",key="#dept.did")
@Transactional
public Dept update(Dept dept) {
int i = deptDao.updateById(dept);
int c=10/0;
return dept;
}
}
并在主启动类开启注解驱动
@EnableCaching //开启缓存注解驱动
@EnableTransactionManagement //开启事务注解驱动
2.Redis实现分布式锁
如果用户过多并且同时发起请求的话,通过代理服务器将这些请求分发给服务器,多台服务器的线程同时进行,会导致数据的准确性丢失。列如一个买票的程序,100个用户同时进行了下单,有两条线程同时拿到了最后一张票,此时,票应该归谁。为避免这种事情发生,可以使用锁来控制线程,但是普通的自动锁只针对当前服务器的jvm,如果是一台服务器确实可以通过自动锁来解决,但当前是集群,也就是说大于1台的服务器,服务器之间没有办法共享这个锁资源,还是会导致数据的准确性丢失。
如何通过Redis实现分布式锁?
首先,所需:一台Nginx代理服务器,两台服务器,jmeter测压,测压就相当于高并发。
先创建一个买票的Springboot项目,然后编辑服务器配置,复制一份该配置,修改一个端口号即可代替两台的服务器,将这两个服务都跑起来。然后前往nginx的配置文件中。
upstream redis {
server localhost:8088; #修改为服务的端口
server localhost:8089; #修改为另一个服务的端口
}
server {
listen 8087; #需要监听的端口
server_name localhost;
location / {
proxy_pass http://redis;
}
}
通过访问这个需要监听的端口来访问你的服务。现在打开jmeter进行测压
测试可得会出现负数。使用Redis解决。
public String decrement(Integer productid) {
ValueOperations<String, String> forValue = redisTemplate.opsForValue();
//调用setIfAbsent方法判断key是否存在,如果存在表示有线程正在执行,没有则继续
Boolean ifAbsent = forValue.setIfAbsent("productid::" + productid, "0", 30, TimeUnit.SECONDS);
if(ifAbsent){
try{
int num = stockDao.findById(productid);
if (num > 0) {
stockDao.update(productid);
System.out.println("商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个");
return "商品编号为:" + productid + "的商品库存剩余:" + (num - 1) + "个";
} else {
System.out.println("商品编号为:" + productid + "的商品库存不足。");
return "商品编号为:" + productid + "的商品库存不足。";
}
}finally {
//线程执行完移除key,表示该线程执行完了,解锁
redisTemplate.delete("productid::" + productid);
}
}else {
//如果有线程再执行,其他线程会进入这里
return "服务器繁忙,请稍后再试";
}
}