Redis实例应用(应用场景+分布式锁)
当缓存数据存入到Redis中,下次在访问相同的数据时,就不在直接操作数据库,直接从Redis中取缓存数据
【注:我们通常会把查询频率高的数据、修改频率低的数据、数据安全性要求不高的数据放入Redis中】
【注:缓存可以提高我们的查询效率,可以降低数据库的访问频率,减少数据库的压力】
示意图:
1.2使用Redis作为缓存
1.2.1 自己手写Redis缓存非业务代码
准备:
新建springboot工程,记得勾选以下的依赖包
创建好后我这里将版本改成2.3.2.RELEASE
如果你们要跟我一样的版本,那么刚才所勾选的mysql是不兼容的,所以需要改动依赖
而且我们也需要MybatisPlass,所以也需要加MybatisPlass依赖
<!-- 2.3.2版本下的mysql依赖 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- MybatisPlass依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
application文件中添加配置:
#mysql连接数据库信息
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/aaasql?serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#redis
spring.redis.host=192.168.235.135
spring.redis.port=6379
#mybatis-plus
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName(value = "tbl_student")
public class student {
@TableId(type = IdType.AUTO)
private int id;
@TableField(value = "s_name")
private String sname;
private String sex;
private int cid;
}
vo-Result实体类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Retust {
private int code;
private String msg;
private Object data;
}
dao层:
public interface StudentDao extends BaseMapper<student> {
}
service层与service实现类:
public interface StudentService {
Retust seleid(Integer id);
}
@Service
public class StudentServiceim implements StudentService {
@Autowired
private StudentDao studentDao;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Retust seleid(Integer id) {
//首先从Redis中查询数据
Object o = redisTemplate.opsForValue().get("stu::" + id);
//判断缓存中是否存在,存在就直接将数据返回回去
if (o != null && o instanceof student){
return new Retust(200,"查询成功",o);
}
//判断数据库中是否存在所要查询的结果,如果有就将结果返回且将结果存入到缓存中
student student = studentDao.selectById(id);
if (student != null){
redisTemplate.opsForValue().set("stu::"+id,student);
return new Retust(200,"查询成功",student);
}
return new Retust(500,"查询失败",null);
}
}
controller层:
@RestController
public class StudentController {
@Autowired
private StudentService service;
@GetMapping("/sele")
private Retust sele(Integer id){
return service.seleid(id);
}
}
运行测试:
【注:第一次运行,缓存中并没有要查找的数据,所以需要从数据库中查找数据,查找成功后,其将数据返回且存入了缓存中】
【注:那么第二次查询,其将会直接从缓存中取出数据返回】
1.2.1 springboot提供注解来实现Redis缓存
【@Cacheable(cacheNames = "key",key = "#id")】--查询缓存注解
【@CachePut(cacheNames = "key",key="#student.id")】--修改缓存注解
【@CacheEvict(cacheNames = "key",key = "#id")】--删除缓存注解
性能更好,更加人性化
【注:首先必须让redisTemplate支持缓存】
在已有的config包中的RedisConfig文件中【没有创建一个】
@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;
}
在主启动类上添加开启缓存注解
@SpringBootApplication
@MapperScan(basePackages = "com.aaa.dao")
@EnableCaching //开启缓存注解驱动
public class RedisSpringBoot02Application {
public static void main(String[] args) {
SpringApplication.run(RedisSpringBoot02Application.class, args);
}
}
在业务层使用该注解: @Cacheable(cacheNames = "key 的名字",key = "#id")
@Service
public class StudentServiceim implements StudentService {
@Autowired
private StudentDao studentDao;
//该注解用于查询功能方法上。 先查询缓存中是否存在cacheNames+"::"+key是否存在,
// 如果存在则不执行该方法,如果不存在则执行该方法并把该方法的返回结果存入缓存中。以cacheName+"::"+key作为缓存的key
@Cacheable(cacheNames = "stu",key = "#id")
public Retust seleid(Integer id) {
student student = studentDao.selectById(id);
return student!=null?new Retust(200,"查询成功",student):new Retust(500,"查询失败",null);
}
}
测试运行:
结果与手动的一样,当缓存中没有查询所需的结果就会去数据库中查找返回,且将结果存入缓存中,第二次访问时,就会直接从redis中取所需的值
这里只演示了查询,剩下的只需大家一一测试即可
完整增删改查:【注:service接口+service实现类】
public interface StudentService {
//查询
Retust seleid(Integer id);
//修改
Retust updata(student student);
//添加
Retust insert(student student);
//删除
Retust del(Integer id);
}
@Service
public class StudentServiceim implements StudentService {
@Autowired
private StudentDao studentDao;
//该注解用于查询功能方法上。 先查询缓存中是否存在cacheNames+"::"+key是否存在,
// 如果存在则不执行该方法,如果不存在则执行该方法并把该方法的返回结果存入缓存中。以cacheName+"::"+key作为缓存的key
@Cacheable(cacheNames = "stu",key = "#id")
public Retust seleid(Integer id) {
student student = studentDao.selectById(id);
return student!=null?new Retust(200,"查询成功",student):new Retust(500,"查询失败",null);
}
//更改
//把该方法的返回结果重新写入到缓存中
@CachePut(cacheNames = "stu",key="#student.id")
public Retust updata(student student) {
int i = studentDao.updateById(student);
return i!=0?new Retust(200,"修改成功",student):new Retust(500,"修改失败",null);
}
//插入 [注:添加不需要进行缓存,因为添加过后的数据会通过查询来直接插入到缓存中]
@Override
public Retust insert(student student) {
int i = studentDao.insert(student);
return i!=0?new Retust(200,"添加成功",student):new Retust(500,"添加失败",null);
}
//删除
//把缓存中的数据进行移除,以cacheName+"::"+key作为缓存的key移除
@CacheEvict(cacheNames = "stu",key = "#id")
public Retust del(Integer id) {
int i = studentDao.deleteById(id);
return i!=0?new Retust(200,"删除成功",null):new Retust(500,"删除失败",null);
}
}
【注;可以继续使用上面的springboot工程模板】
实体类:
@Data
@TableName("tbl_stock")
public class Stock {
@TableId(type = IdType.AUTO)
private int id;
private int goodsnum;
}
dao层:
@Mapper
public interface StockDao {
@Select("select goodsnum from tbl_stock where id=#{id}")
int findById(Integer productid);
@Update("update tbl_stock set goodsnum=goodsnum-1 where id=#{id} ")
void update(Integer productid);
}
service层:
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
//---通过jmeter压测后发现商品出现--线程安全问题。
//--如何解决上面的线程安全问题: 加锁。【自动锁synchronized 或手动锁 Lock】
//--如果我们现在的项目部署时为一个集群--如果再高并发下使用【自动锁synchronized 或手动锁 Lock】有出现了线程安全问题。【自动锁synchronized 或手动锁 Lock】他们属于jvm锁。
//--如何解决集群下线程安全问题。
public String decrement(Integer productid) {
synchronized (this) {
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 + "的商品库存不足。";
}
}
}
}
controller:
@RestController
public class StockController {
@Autowired
private StockService02 stockService;
//根据商品编号减库存
@GetMapping("/incr/{productid}")
public String incr(@PathVariable Integer productid){
return stockService.decrement(productid);
}
}
测试运行:
现在只是单一测试,在实际应用场景中,有时可能会达到几万的并发量,这里就可能会出现超卖的情况
2.1 模拟多服务
现在我们模拟多服务,多线程并发的场景
这里使用两款工具:
第一,我们需要两台服务共同运行
启动两台服务器
2.2使用Nginx代理
这里使用windows版Nginx软件
Nginx百度云盘链接 提取码:6666
下载解压后
打开里面的conf目录,打开nginx.conf配置文件
将我们两台服务代理
#定义被负载均衡的所有服务器地址【upstream: 定义集群信息】
upstream aaa{
server localhost:8088;
server localhost:8089;
}
#定义负载均衡代理端口
server {
listen 83;
server_name localhost;
location /{
proxy_pass http://aaa;
}
}
2.3使用JMeter工具
【注:使用Jmeter工具用来模拟多用户多线程并发场景】
jmerer百度云盘链接 提取码:6666
下载后解压
打开文件目录进入到bin目录中,打开jmeter.bat文件进入
点击运行
【注:前提是你的idea两台服务都要启动】
2.4使用线程自动锁测试结果
果然,在多线程的并发下,出现了超卖现象,这种是及不被允许的
【注:这里是已经使用线程自动锁的情况下】
2.5使用Redis分布式锁
service层代码:
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
@Autowired
private RedisTemplate redisTemplate;
//---通过jmeter压测后发现商品出现--线程安全问题。
//--如何解决上面的线程安全问题: 加锁。【自动锁synchronized 或手动锁 Lock】
//--如果我们现在的项目部署时为一个集群--如果再高并发下使用【自动锁synchronized 或手动锁 Lock】有出现了线程安全问题。【自动锁synchronized 或手动锁 Lock】他们属于jvm锁。
//--如何解决集群下线程安全问题。
public String decrement(Integer productid) {
ValueOperations valueOperations = redisTemplate.opsForValue();
//使用opsForValue()中的setIfAbsent方法来实现Redis分布式锁
Boolean flss = valueOperations.setIfAbsent("pro::" + productid, "test", 30, TimeUnit.SECONDS);
//判断是否已经拿到锁,true为已拿到锁
if (flss){
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 {
//释放锁资源
redisTemplate.delete("pro::"+productid);
}//如果没有拿到锁进行以下尝试
}else {
//休眠100毫秒再次尝试调用锁
try {
Thread.sleep(100);
}catch (InterruptedException e){
e.printStackTrace();
}
//递归锁
return decrement(productid);
}
}
}
再次测压运行测试:
分布式锁成功起到作用
Redis分布式锁测试成功!!!
3.but
【注:分布式锁有一处缺陷,在实际应用场景中,哪怕一处小小的缺陷都能影响业务】
【注:Redis分布式锁,当我们的程序执行时间超过redis锁的时间时,会出现bug,当出现此现象后,似乎问题又回到了原来的情况】
缺陷解决:
使用第三方插件--redisson
3.1 Redisson
【注:redisson[基于redis完成的--提供了一个看门狗机制】
Redisson看门狗流程示意图:
3.2 使用redisson:
3.2.1 添加Redisson依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.23.4</version>
</dependency>
3.2.2 创建一个对象
创建一个RedissonClient对象,且交于spring容器管理
这里就直接创建一个RedissonConfig配置类
@Configuration
public class RedissonConfig {
@Bean //返回的对象交于spring容器来管理
public RedissonClient redissonClient(){
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.223.158:6379");
RedissonClient redissonClient = Redisson.create(config);
return redissonClient;
}
}
3.2.3 使用RedissonClient对象
@Service
public class StockService02 {
@Autowired
private StockDao stockDao;
@Autowired
private RedissonClient redissonClient;
//---通过jmeter压测后发现商品出现--线程安全问题。
//--如何解决上面的线程安全问题: 加锁。【自动锁synchronized 或手动锁 Lock】
//--如果我们现在的项目部署时为一个集群--如果再高并发下使用【自动锁synchronized 或手动锁 Lock】有出现了线程安全问题。【自动锁synchronized 或手动锁 Lock】他们属于jvm锁。
//--如何解决集群下线程安全问题。
public String decrement(Integer productid) {
//获取指定的锁对象
RLock rlock = redissonClient.getLock("product::" + productid);
//加锁
rlock.lock(30,TimeUnit.SECONDS);
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 {
//释放锁资源
rlock.unlock();
}
}
}
测试的话再次压力测试就可
4.长见的Redis问题
4.1 redis工作中的使用场景
1. 可以作为热点缓存数据库
2. 可以解决分布式锁
3. 作为限时任务的操作
4. 热门商品的排行排行榜等
4.2 redis支持的数据类型
redis支持很多数据类型,而我们再工作中使用最多的是String,Hash,List,Set,SortedSet
4.3 redis持久化方式
RDB:快照存储,每个一段时间对redis内存中的数据进行快照存在。
AOF:日志追加。当执行写操作时会通过write函数记录到日志文件中。
4.4 redis缓存穿透?以及如何解决缓存穿透?
什么是缓存穿透:数据库中没有该数据,缓存中也没有该数据,这时有人恶意访问这种数据。
解决方案:
<1>在控制层对一些不合法的数据进行校验。
<2>使用布隆过滤器。把数据库中存在的id放入一个大的bitmap数组中,当查询一个不存在的id时就会被该过滤器过滤掉。
<3>我们把数据中查询的空对象也存入缓存中。但是这个对象的存储时间不能太长-一般不超过5分钟
4.5 redis缓存雪崩?以及如何解决缓存雪崩?
1. 什么是缓存雪崩? 所谓的缓存雪崩就是缓存中出现大量数据过期的现象,而就在这时有大量的请求访问这些数据。压力顶到数据库。从而造成数据库压力过大。
<1>项目刚上线。---预先把数据存放的缓存中
<2>缓存中的数据在某个时间端内出现大量过期。 --设置散列的过期时间。
<3>redis宕机----搭建redis集群
4.6 如何保证缓存数据和数据库数据一致
1. 合理的设置缓存的过期时间
2. 当执行CUD操作时,要同步修改缓存数据。
4..7 redis内存淘汰策略
【注:修改redis.conf配置文件可以改变淘汰策略】
以上便是Redis实例应用(应用场景+分布式锁)中的内容,如有漏缺请在下方留言告知,我会及时补充
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)
· 从HTTP原因短语缺失研究HTTP/2和HTTP/3的设计差异