黑马redis实战篇-商铺缓存

news/2024/11/17 6:30:52/

目录

五、实战篇-商户查询缓存

5.1 什么是缓存

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

2、添加redis时,数据查询的作用模型:

3、业务流程图:​编辑

4、代码实现

5、练习题

5.3 缓存更新策略

1、主动更新

2.Cache Aside Pattern(旁路缓存模式)

3、总结

4、给查询商铺的缓存添加超时剔除和主动更新的策略

5.4 缓存穿透

1、解决方案

2、解决商铺查询时,缓存穿透问题

3、总结

5.5 缓存雪崩

5.6 缓存击穿

1、解决方案

2、基于互斥锁方式解决缓存击穿问题

3、基于逻辑过期方式解决缓存击穿问题

4、JMeter下载和安装

5.7 缓存工具封装


五、实战篇-商户查询缓存

5.1 什么是缓存

缓存就是数据交换的缓冲区(称作Cache ),是存储数据的临时地方,一般读写性能较高

 

缓存的作用:

  • 降低后端负载 ---直接访问缓存,返回数据

  • 提高读写效率,降低响应时间---基于内存存储

缓存的成本:

  • 数据一致性成本

  • 代码维护成本----解决一致性问题代码复杂

  • 运维成本-- 要保证高可用搭建集群

5.2 添加Redis缓存

1、不添加redis时,数据查询的作用模型:

 

2、添加redis时,数据查询的作用模型:

redis命中直接返回数据,未命中数据库查询返回数据,并且将数据缓存到redis中

 

3、业务流程图:

 

4、代码实现

 public Result selectShopInfoById(Long id) {String key = CACHE_SHOP_KEY + id;//1.判断redis中是否存在该id的数据String str = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(str)) {//2.存在 直接返回数据return Result.ok(JSONUtil.toBean(str,Shop.class));}//3.不存在 查询数据库是否存在Shop shop = baseMapper.selectById(id);if (StringUtils.isEmpty(shop)) {//4.不存在直接返回 404return Result.fail("店铺不存在");}//5.存在将数据存储在redis,然后返回String shopJsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,shopJsonStr);return Result.ok(shop);}

5、练习题

给店铺类型业务添加缓存

 

public Result queryOrderByAscList() {//1.判断redis是否存在 商铺类型的缓存Long size = stringRedisTemplate.opsForList().size(CACHE_SHOP_TYPE_KEY);if (size > 0){//2.存在 直接取出返回List<String> range = stringRedisTemplate.opsForList().range(CACHE_SHOP_TYPE_KEY, 0, size);List<ShopType> shopTypes = range.stream().map(item -> {return  JSONUtil.toBean(item,ShopType.class);}).collect(Collectors.toList());//.sorted(Comparator.comparing(ShopType::getSort).reversed())return Result.ok(shopTypes);}//3.不存在,查询数据库QueryWrapper<ShopType> queryWrapper = new QueryWrapper<>();queryWrapper.orderByAsc("sort");List<ShopType> shopTypes = baseMapper.selectList(queryWrapper);if (shopTypes.size() <= 0){//4.数据库不存在 直接返回错误return Result.fail("数据不存在");}//5.数据库存在,将数据存储在缓存中然后返回List<String> collect = shopTypes.stream().map(item -> {return JSONUtil.toJsonStr(item);}).collect(Collectors.toList());stringRedisTemplate.opsForList().rightPushAll(CACHE_SHOP_TYPE_KEY,collect);//设置过期时间是1天stringRedisTemplate.expire(CACHE_SHOP_TYPE_KEY,CACHE_SHOP_TYPE_TTL, TimeUnit.MINUTES);return Result.ok(shopTypes);}

5.3 缓存更新策略

内存淘汰超时剔除主动更新
说明不用自己维护,利用Redis的内存淘汰机制,当内存不足时自动淘汰部分数据,下次查询时更新缓存给缓存数据添加TTL过期时间,到期后自动删除缓存。下次查询时更新缓存编写业务逻辑,在修改数据的同时,更新缓存
一致性一般
维护成本

业务场景:

  • 低一致性需求:使用内存淘汰机制。例如店铺类型的查询缓存

  • 高一致性需求:主动更新,并以超时时间作为兜底方案。例如店铺详情查询的缓存

1、主动更新

  • Cache Aside Pattern 有缓存的调用缓存,在更新数据库的同时更新缓存

  • Read/Write Through Pattern 缓存和数据库整合为一个服务,由服务来维护一致性。调用者调用该服务,无序关心缓存一致性问题

  • Write Behind Caching Pattern 调用者只操作缓存,由其他线程异步的将缓存数据持久化到数据库,保证最终一致

Redis的主动更新有三种常见的方案,包括:
​
Cache Aside Pattern(旁路缓存模式):应用程序先从缓存中获取数据,如果缓存中不存在要访问的数据,则从数据库获取,再将数据写入缓存中。
优点:高效性能,减少数据库访问次数和负载,适合于对数据实时性要求不高的应用。
​
缺点:存在缓存和数据库数据不一致的问题,当读写并发量大时,可能会出现脏数据。
​
Read/Write Through Pattern(读写穿透模式):数据缓存和数据库相连,应用程序从缓存中获取数据,如缓存中没有相应数据,会通过缓存访问层查找数据。该层在未命中数据后,查询数据库。若命中则返回数据,并同步写入缓存中;否则返回空值或默认值。
优点:保证缓存、数据库数据一致性,并且在缓存失效的情况下,也可以避免因读操作而引起的数据库压力过大,同时也可以防止缓存数据与数据库之间的数据不一致。
​
缺点:每次访问数据都必须通过缓存去访问数据库,增加了结构的复杂性并降低了系统的效率。
​
Write Behind Caching Pattern(写回缓存模式):在进行写操作时,不直接将数据写入到数据库中,而是先将数据写入缓存中,待缓存达到一定条件后再批量同步到数据库中。
优点:提高了写操作的性能,并且降低了数据库负载,可以适用于写入比较频繁但读取全量较少的应用场景,同时也减少了与数据库的交互次数和延迟。
​
缺点:由于只在达到缓存阈值之后才进行同步,因此可能会存在缓存中未及时更新的数据,从而引起数据不一致性问题,同时当缓存重新启动时还需要从磁盘上读取数据进行恢复,增加了复杂度。
​
需要针对具体应用场景选择合适的主动更新方案,并结合Redis中提供的其他功能一起使用。

这里比较常用的:Cache Aside Pattern(旁路缓存模式)

2.Cache Aside Pattern(旁路缓存模式)

操作缓存和数据库有三个问题需要考虑:

  1. 删除缓存还是更新缓存?

    • 更新缓存:每次更新数据库都更新缓存,无效写操作较多

    • 删除缓存:更新数据库时让缓存失败,查询时再更新缓存(比较符合)

  2. 如何保障缓存与数据库的操作同时成功或失败?

    • 单体系统,将缓存与数据库操作放在一个事务

    • 分布式系统,利用TCC等分布式事务方案

  3. 先操作缓存还是先操作数据库?

    • 先删除缓存,再操作数据库(不推荐,更新数据库时间长,出现概率很大)

      第一个线程删除缓存后,在更新数据库的时候,还没更新成功的时候,
      ​
      第二个线程访问了,发现缓存没有,查询数据库的数据,这是数据库的数据的旧的,将旧的数据更新到缓存中出现了不一致性

       

      可以使用延时双删的策略,即先删除缓存,在更新数据库,然后休眠500毫秒在删除缓存,但是因为第二次延时时间,不确定性很大,一般不推荐使用

    • 先操作数据库,再删除缓存(推荐,相较于上一种出现概率很低)

      因为某种原因,缓存找中数据没了,线程1访问的时候发现没有缓存,查询数据库得到旧数据,要进行写入缓存操作时
      线程2进行了更新数据库,删除缓存,然后线程1更新了缓存为旧数据

       

3、总结

缓存更新策略的最佳实践方案:

  1. 低一致性需求:使用Redis自带的内存淘汰机制

  2. 高一致性需求:主动更新,并以超时剔除作为兜底方案

    • 读操作 Cache Aside Pattern(旁路缓存模式):

      • 缓存未命则直接返回

      • 缓存未命中则查询数据库,并写入缓存,设定超时时间

    • 写操作:

      • 先写数据库,然后再删除缓存

      • 要确保数据库与缓存操作的原子性

4、给查询商铺的缓存添加超时剔除和主动更新的策略

修改ShopController中的业务逻辑,满足下面的需求:

  1. 根据id查询商铺时,如果缓存未命中,则查询数据库,将数据库结果写入缓存,并设置超时时间

    @Overridepublic Result selectShopInfoById(Long id) {String key = CACHE_SHOP_KEY + id;//1.判断redis中是否存在该id的数据String str = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(str)) {//2.存在 直接返回数据return Result.ok(JSONUtil.toBean(str,Shop.class));}//3.不存在 查询数据库是否存在Shop shop = baseMapper.selectById(id);if (StringUtils.isEmpty(shop)) {//4.不存在直接返回 404return Result.fail("店铺不存在");}//5.存在将数据存储在redis,然后返回String shopJsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);}

  2. 根据id修改店铺时,先修改数据库,再删除缓存

     @Override@Transactionalpublic Result updateShopById(Shop shop) {Long id = shop.getId();if (id == null) {return Result.fail("店铺id不能为空");}//修改数据库baseMapper.updateById(shop);//删除缓存stringRedisTemplate.delete(CACHE_SHOP_KEY+shop.getId());return Result.ok();}

5.4 缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。

1、解决方案

常见的解决方案有两种:

  • 缓存空对象

    • 优点:实现简单,维护方便

    • 缺点:

      • 额外的内存消耗 ---设置ttl过期时间

      • 可能造成短期的不一致 ----插入数据的时候,更新缓存将null的覆盖

         

  • 布隆过滤

    • 优点:内存占用较少,没有多余key

    • 缺点:

      • 实现复杂

      • 存在误判可能---布隆过滤器是居于hash算法,存在哈希碰撞问题

        判断不存在的肯定不存在,判断存在的时候,可能不存在

         

2、解决商铺查询时,缓存穿透问题

 

public Result selectShopInfoById(Long id) {String key = CACHE_SHOP_KEY + id;//1.判断redis中是否存在该id的数据String str = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(str)) {//2.存在 直接返回数据return Result.ok(JSONUtil.toBean(str,Shop.class));}//上面判断后 执行到这句的时候,只能是null或者空字符串if (str != null) {return Result.fail("店铺不存在");}
​//3.不存在 查询数据库是否存在Shop shop = baseMapper.selectById(id);if (StringUtils.isEmpty(shop)) {//将null存入到redis中stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//4.不存在直接返回 404return Result.fail("店铺不存在");}//5.存在将数据存储在redis,然后返回String shopJsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);return Result.ok(shop);}

3、总结

缓存穿透产生的原因是什么?

  • 用户请求的数据在缓存和数据库汇总都不存在,不断发起这样的请求给数据库带来巨大压力

缓存穿透的解决方案有那些?

  • 缓存null值

  • 布隆过滤器

  • 增强id的复杂度,避免被猜测id规律,然后做好数据的基础格式校验

  • 加强用户权限校验

  • 做好热点参数的限流

5.5 缓存雪崩

缓存雪崩是指同一时段大量的缓存key同时失效或者Redis服务五宕机,导致大量请求到达数据库,带来巨大压力。

解决方案:

  • 给不同的key的TTL添加随机值

  • 利用Redis集群提高服务的可用性

  • 给缓存业务添加降级限流策略

  • 给业务添加多级缓存

     

5.6 缓存击穿

缓存击穿问题也叫作热点key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大冲击。

 

1、解决方案

互斥锁

 

逻辑过期

 

比较

解决方案优点缺点
互斥锁没有额外的内存消耗 保证了一致性 实现简单线程需要等待,性能受影响 可能有死锁的情况
逻辑过期线程无序等待,性能好不保证一致性 存在内存消耗 实现复杂

2、基于互斥锁方式解决缓存击穿问题

需求:根据id查询商铺的业务,基于互斥锁方式来解决缓存击穿问题

 

 private  Result cacheShopWithMutex(Long id) {String key = CACHE_SHOP_KEY + id;Shop shop = null;//1.判断redis中是否存在该id的数据String str = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isNotBlank(str)) {//2.存在 直接返回数据return Result.ok(JSONUtil.toBean(str,Shop.class));}//上面判断后 执行到这句的时候,只能是null或者空字符串if (str != null) {return Result.fail("店铺不存在");}String lockKey = LOCK_SHOP_KEY + id;try {
​//3.不存在 先尝试获取互斥锁  利用redis中string字符串中setBoolean flagBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);boolean flag = BooleanUtil.isTrue(flagBoolean);//4.获取锁失败if (!flag) {//获取锁失败休眠一会Thread.sleep(100);//然后进行重试 ---递归return selectShopInfoById(id);}//5.如果获取锁成功 查询数据库shop = baseMapper.selectById(id);//模拟重建延迟Thread.sleep(200);//如果数据库中没有数据if (StringUtils.isEmpty(shop)) {//将null存入到redis中stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);//不存在直接返回 404return Result.fail("店铺不存在");}//.存在将数据存储在redis,然后返回String shopJsonStr = JSONUtil.toJsonStr(shop);stringRedisTemplate.opsForValue().set(key,shopJsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);} catch (InterruptedException e) {e.printStackTrace();}finally {//6.释放锁stringRedisTemplate.delete(lockKey);}return Result.ok(shop);}

3、基于逻辑过期方式解决缓存击穿问题

需求:根据id查询商铺的业务,基于逻辑过期方式来解决缓存击穿问题

 

//弄一个线程池private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);//逻辑过期private Result cacheShopWithLogicTTL(Long id) {String key = CACHE_SHOP_KEY + id;//1.判断redis中是否存在该id的数据String str = stringRedisTemplate.opsForValue().get(key);if (StrUtil.isBlank(str)) {//2.不存在 直接返回空return Result.fail("商铺信息为空");}
​
​//3.存在 判断缓存是否过期  逻辑时间RedisData redisData = JSONUtil.toBean(str, RedisData.class);LocalDateTime expireTime = redisData.getExpireTime();JSONObject data = (JSONObject)redisData.getData();Shop shop = JSONUtil.toBean(data, Shop.class);if (expireTime.isAfter(LocalDateTime.now())) {//4.未过期 直接返回商铺信息return Result.ok(shop);}//5.过期了尝试获取互斥锁String lockKey = LOCK_SHOP_KEY + id;Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);boolean flag = BooleanUtil.isTrue(aBoolean);if (!flag) {//6.如果未获取到锁 直接返回旧数据return Result.ok(shop);}//7.成功获取到锁 开启一个独立线程CACHE_REBUILD_EXECUTOR.submit(() -> {//重构缓存try {saveShopToRedis(id,30L);} catch (InterruptedException e) {e.printStackTrace();}finally {stringRedisTemplate.delete(lockKey);}//释放锁});//8.返回旧的数据return Result.ok(shop);}

4、JMeter下载和安装

参考

JMeter下载和安装_仰望_1的博客-CSDN博客

1.下载

2.解压

 

 

3.设置环境变量

 

4.path中设置

 

5.启动

双击打开bin中的jemter.bat

 

就自动启动了

 

6.设置中文

 

7.进行配置

 

 

 

8、输入参数,测试

 

 

 

5.7 缓存工具封装

基于StingRedisTemplate封装一个缓存工具类,满足下列需求:

方法1:将任意Java对象序列化为json并存储在String类型的key中,并且可设置TTL过期时间

方法2:将任意Java对象序列化为json并存储在String类型的key中,并在可以设计逻辑过期时间,用于处理缓存击穿问题

方法3:根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题

方法4:根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题

封装类:CacheClient

package com.hmdp.utils;
​
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
​
import javax.annotation.Resource;
import java.time.LocalDateTime;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
​
import static com.hmdp.utils.RedisConstants.*;
​
/*** @packageName: com.hmdp.utils* @author: winter* @date: 2023/4/25 8:55* @version: 1.0* @email 1660420659@qq.com* @description: 封装Redis工具类*/
@Slf4j
@Component
public class CacheClient {
​@Resourceprivate StringRedisTemplate stringRedisTemplate;
​/*** 将任意Java对象序列化为json并存储在String类型的key中,* 并且可设置TTL过期时间* @param key  key* @param obj  存储对象* @param timeTTL  过期时间* @param timeUnit  单位*/public void set(String key, Object obj, Long timeTTL, TimeUnit timeUnit) {stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(obj),timeTTL,timeUnit);}
​/*** 将任意Java对象序列化为json并存储在String类型的key中,* 并在可以设计逻辑过期时间,用于处理缓存击穿问题* @param key  key* @param obj  存储对象* @param timeTTL  过期时间* @param timeUnit  单位*/public  void setWithLogicalExpire(String key,Object obj,Long timeTTL, TimeUnit timeUnit) {RedisData redisData = new RedisData();redisData.setData(obj);redisData.setExpireTime(LocalDateTime.now().plusSeconds(timeUnit.toSeconds(timeTTL)));stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(redisData));}
​/*** 通过key获取字符串* @param key* @return*/public String get(String key) {String str = stringRedisTemplate.opsForValue().get(key);return str;}
​/*** 根据指定的key查询缓存,并反序列化为指定类型,利用缓存空值的方式解决缓存穿透问题* @param key  key值* @param id  id值* @param tClass  类型* @param function 手写方法* @param timeTTL  过期时间* @param timeUnit  时间单位* @param <T>  对象类型* @param <ID> id类型* @return  对象*/public <T,ID> T getWithPassThrough(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {//1.判断redis中是否存在该id的数据String str = get(key);if (StrUtil.isNotBlank(str)) {//2.存在 直接返回数据return JSONUtil.toBean(str,tClass);}//上面判断后 执行到这句的时候,只能是null或者空字符串if (str != null) {return null;}
​//3.不存在 查询数据库是否存在T shop = function.apply(id);if (StringUtils.isEmpty(shop)) {//将null存入到redis中set(key,"",CACHE_NULL_TTL,timeUnit);//4.不存在直接返回 404return null;}//5.存在将数据存储在redis,然后返回set(key,shop,timeTTL,timeUnit);return shop;}
​
​//弄一个线程池private static  final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
​/*** 根据指定的key查询缓存,并反序列化为指定类型,需要利用逻辑过期解决缓存击穿问题* @param key key* @param id id* @param tClass  RedisDate中存储对象类型* @param function 方法* @param timeTTL 过期时间* @param timeUnit 过期类型* @param <T>  对象类型* @param <ID>  id类型* @return 对象*/public <T,ID> T getWithLogicalExpire(String key,ID id, Class<T> tClass, Function<ID,T> function,Long timeTTL, TimeUnit timeUnit) {//1.判断redis中是否存在该id的数据String str = get(key);if (StrUtil.isBlank(str)) {//2.不存在 直接返回空return null;}
​
​//3.存在 判断缓存是否过期  逻辑时间RedisData redisData = JSONUtil.toBean(str, RedisData.class);LocalDateTime expireTime = redisData.getExpireTime();JSONObject data = (JSONObject)redisData.getData();T shop = JSONUtil.toBean(data, tClass);if (expireTime.isAfter(LocalDateTime.now())) {//4.未过期 直接返回商铺信息return shop;}//5.过期了尝试获取互斥锁String lockKey = LOCK_SHOP_KEY + id;Boolean aBoolean = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, "1", LOCK_SHOP_TTL, TimeUnit.SECONDS);boolean flag = BooleanUtil.isTrue(aBoolean);if (!flag) {//6.如果未获取到锁 直接返回旧数据return shop;}//7.成功获取到锁 开启一个独立线程CACHE_REBUILD_EXECUTOR.submit(() -> {//重构缓存try {//查询店铺数据T tshop = function.apply(id);//模拟Thread.sleep(200);//封装逻辑过期时间setWithLogicalExpire(key,tshop,timeTTL,timeUnit);} catch (InterruptedException e) {e.printStackTrace();}finally {stringRedisTemplate.delete(lockKey);}//释放锁});//8.返回旧的数据return shop;}
}
​

测试:ShopServiceImpl

 @Overridepublic Result selectShopInfoById(Long id) throws InterruptedException {//1.缓存穿透 存储null值解决方案
//     return  cacheShopWithPassThrough(id);//2.缓存击穿  --互斥锁解决方案
//       return cacheShopWithMutex(id);//3.缓存击穿 ---逻辑过期解决方案
//        return cacheShopWithLogicTTL(id);
​//4.使用封装类中解决缓存穿透  存储null值办法
//        Shop shop = cacheClient.getWithPassThrough(CACHE_SHOP_KEY + id, id, Shop.class
//                            ,this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);
//        return Result.ok(shop);
​//5.使用封装类中解决缓存击穿  逻辑过期方式//为了测试 将逻辑过期时间设置短一点Shop shop = cacheClient.getWithLogicalExpire(CACHE_SHOP_KEY + id, id, Shop.class, this::getById, 10L, TimeUnit.SECONDS);return Result.ok(shop);}

具体代码

redis实战篇-hmdp-短信登录-商铺缓存: 存放黑马点评中redis进行短信登录、商铺查询的代码 ,包括前端后后端


http://www.ppmy.cn/news/53197.html

相关文章

MATLAB-Lingo求解线性规划问题-奶制品2

奶制品的生产销售计划&#xff0c;给定条件不变 为了增加工厂的获利&#xff0c;开发了奶制品的深加工技术&#xff1a;用2小时和3元加工费&#xff0c;可将1kgA1加工成0.8kg高级奶制品B1&#xff0c;也可将1kgA2加工成0.75kg高级奶制品B2&#xff0c;每千克B1能获利44元&#…

Java设计模式:工厂模式,优化代码的灵活性和可维护性

Java设计模式&#xff1a;工厂模式&#xff0c;优化代码的灵活性和可维护性 Java设计模式之工厂模式什么是工厂模式&#xff1f;工厂模式的使用总结 Java设计模式之工厂模式 作为一名初级程序员&#xff0c;当你开始接触设计模式的时候&#xff0c;你可能会觉得这些概念很抽象…

适合Java老手阅读的书籍推荐:

《Effective Java》是一本由Java编程语言的核心库开发者之一Joshua Bloch撰写的书籍。这本书涵盖了Java语言中的许多重要的主题和问题&#xff0c;并提供了最佳实践和解决方案。 这本书的核心思想是&#xff0c;通过对Java语言的理解和应用&#xff0c;可以写出更加优秀、高质…

PHP入门基础与实战技巧

PHP是一种较为常见的动态网页开发语言&#xff0c;它广泛应用于服务器端的开发和网站构建。与其他语言相比&#xff0c;PHP易学易用、开发效率高、拓展性强等优点&#xff0c;使之成为了广大开发者的首选。如果您想入门PHP开发&#xff0c;本文将介绍一些必备的基础知识和实战技…

@PostConstruct注解和@PreDestroy注解

前言 Bean注解指定初始化和销毁的方法&#xff0c;也介绍了使用InitializingBean和DisposableBean来处理bean的初始化和销毁。JDK中还提供了两个注解能够在bean创建完成并且属性赋值完成之后执行一些初始化工作和在容器销毁bean之前通知我们进行一些清理工作。 1.PostConstru…

【ES6】ES6一些基本用法:

文章目录 一、从对象obj中取值1、不好的2、好的 二、合并两个数组&#xff0c;合并两个对象1、不好的2、好的 三、拼接字符串1、不好的2、好的 四、关于if中判断条件1、不好的2、好的 五、关于列表搜索六、关于扁平化数组七、关于获取对象属性值八、关于添加对象属性、九、关于…

第一节 ogre源码编译与安装

一. 电脑环境要求 本机使用的编译环境为&#xff1a;系统为Windows 10&#xff0c; Microsoft Visual Studio Enterprise 2019&#xff0c;版本 16.11.26&#xff0c;cmake-3.18.6-win64-x64 这些为基本的操作环境自己可以从网上下载安装。 二. 依赖环境下载 DirectX SDK &a…

详谈Android进程间的大数据通信机制:LocalSocket

前言 说起Android进行间通信&#xff0c;大家第一时间会想到AIDL&#xff0c;但是由于Binder机制的限制&#xff0c;AIDL无法传输超大数据。 比如我们在之前文章《WebRtc中是如何处理视频数据的&#xff1f;》提到的我们可以得到WebRtc的视频数据&#xff0c;这时候我们如果有…