黑马点评part2 -- 商户查询缓存

1 什么是缓存?

前言:什么是缓存?

就像自行车,越野车的避震器

举个例子:越野车,山地自行车,都拥有"避震器",防止车体加速后因惯性,在酷似"U"字母的地形上飞跃,硬着陆导致的损害,像个弹簧一样;

同样,实际开发中,系统也需要"避震器",防止过高的数据访问猛冲系统,导致其操作线程无法及时处理信息而瘫痪;

这在实际开发中对企业讲,对产品口碑,用户评价都是致命的;所以企业非常重视缓存技术;

缓存(Cache),就是数据交换的缓冲区,俗称的缓存就是缓冲区内的数据,一般从数据库中获取,存储于本地代码(例如:

例1:Static final ConcurrentHashMap<K,V> map = new ConcurrentHashMap<>(); 本地用于高并发
​
例2:static final Cache<K,V> USER_CACHE = CacheBuilder.newBuilder().build(); 用于redis等缓存
​
例3:Static final Map<K,V> map =  new HashMap(); 本地缓存

由于其被Static修饰,所以随着类的加载而被加载到内存之中,作为本地缓存,由于其又被final修饰,所以其引用(例3:map)和对象(例3:new HashMap())之间的关系是固定的,不能改变,因此不用担心赋值(=)导致缓存失效;

1.1 为什么要使用缓存

一句话:因为速度快,好用

缓存数据存储于代码中,而代码运行在内存中,内存的读写性能远高于磁盘,缓存可以大大降低用户访问并发量带来的服务器读写压力

实际开发过程中,企业的数据量,少则几十万,多则几千万,这么大数据量,如果没有缓存来作为"避震器",系统是几乎撑不住的,所以企业会大量运用到缓存技术;

但是缓存也会增加代码复杂度和运营的成本:

1.2 如何使用缓存

实际开发中,会构筑多级缓存来使系统运行速度进一步提升,例如:本地缓存与redis中的缓存并发使用

浏览器缓存:主要是存在于浏览器端的缓存

应用层缓存:可以分为tomcat本地缓存,比如之前提到的map,或者是使用redis作为缓存

数据库缓存:在数据库中有一片空间是 buffer pool,增改查数据都会先加载到mysql的缓存中

CPU缓存:当代计算机最大的问题是 cpu性能提升了,但内存读写速度没有跟上,所以为了适应当下的情况,增加了cpu的L1,L2,L3级的缓存

2 添加商户缓存

在我们查询商户信息时,我们是直接操作从数据库中去进行查询的,大致逻辑是这样,直接查询数据库那肯定慢咯,所以我们需要增加缓存

@GetMapping("/{id}")
public Result queryShopById(@PathVariable("id") Long id) {
    //这里是直接查询数据库
    return shopService.queryById(id);
}

2.1 、缓存模型和思路

标准的操作方式就是查询数据库之前先查询缓存,如果缓存数据存在,则直接从缓存中返回,如果缓存数据不存在,再查询数据库,然后将数据存入redis。

2.1 、代码如下

代码思路:如果缓存有,则直接返回,如果缓存不存在,则查询数据库,然后存入redis。

在前端可以发现速度明显加快 : 

3 . 添加商户类型缓存

        对于店铺类型的数据,一般来说变动是很小的,那么可以添加到redis中进行缓存,提高效率 ;

完整代码 (利用String类型实现):

@Service
public class ShopTypeServiceImpl extends ServiceImpl<ShopTypeMapper, ShopType> implements IShopTypeService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;
    
    @Override
    public Result queryType() {
        // 1 . 先查询redis缓存
        String typeList = stringRedisTemplate.opsForValue().get(CACHE_SHOP_TYPE_KEY) ;
        // 2 . 判断是否缓存命中
        if(StrUtil.isNotBlank(typeList)){
            // 2 . 1 存在,直接返回
            List<ShopType> list = JSONUtil.toList(typeList,ShopType.class) ;
            return Result.ok(list) ;
        }
        // 2 . 2 缓存未命中,查数据库
        List<ShopType> list = query().orderByAsc("sort").list() ;

        // 3 . 判断数据库中是否存在
        if(list == null){
            // 3 . 1 数据库也为空 , 直接返回false ;
            return Result.fail("分类不存在") ;
        }
        // 3 . 2 数据库中存在 , 则将查询到的数据存入redis中
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_TYPE_KEY , JSONUtil.toJsonStr(list)) ;
        // 3 . 3 返回
        return Result.ok(list) ;

    }
}

这里要注意的一个小错误就是 : 在Controller中返回,直接返回typeService.queryType()即可,因为你的service中的方法返回的结果就是一个Result.ok(lsit),否者可能会出现前端不显示商户分类的情况 : 

 实现效果 : 

4 . 缓存更新策略 

缓存更新策略 : 

对于主动更新有三种方案 : 

第三种,对于数据一致性难以维护 ;

一般采取第一种 : 

对于Cache Aside Pattern需要考虑的三个问题 : 

对于两种策略 : 

从线程安全的角度上来讲,第二种发生问题的概率小,后面再加上一个超时时间即可 ;

总结  

5 . 实现商铺和缓存与数据库双写一致

修改的核心思路 : 

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

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

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

查询代码修改 : 

加一个过期时间即可 : 

    @Override
    public Result queryById(Long id) { // 根据id查商户
        String key = CACHE_SHOP_KEY + id ;
        // 1 . 从redis中查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2 . 判断是否存在
        if(StrUtil.isNotBlank(shopJson)) {
            // 3 . 存在 , 根据id查询数据库
            Shop shop = JSONUtil.toBean(shopJson,Shop.class) ;
            return Result.ok(shop) ;
        }
        // 4 . 不存在 , 根据id查询数据库
        Shop shop = getById(id) ;
        // 4 .  不存在  , 返回错误
        if(shop==null){
            return Result.fail("店铺不存在!") ;
        }
        // 6 . 存在 , 写入redis
        // 加入缓存过期时间
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

测试 : 查询之后 , redis中出现相应数据,并且时间刷新 : 

对于修改用户 :


    @Override
    @Transactional
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id == null){
            return Result.fail("店铺id不能为空") ;
        }
        // 1 . 更新数据库
        updateById(shop) ;
        // 2 . 删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY + shop.getId()) ;

        return Result.ok() ;

    }
  •  首先更新数据库  , 然后删除缓存 ;
  • 需要加上@Transactional注解,来保持数据的一致性;

测试,这里需要用postman工具来测试,本次测试修改名字 (原本103修改为102): 

{
    "area":"大关",
    "openHours":"10:00-22:00",
    "sold":4215,
    "address":"金华路29号",
    "comments":3035,
    "avgPrice":80,
    "score":37,
    "name":"102茶餐厅",
    "typeId":1,
    "id":1
}

首先发现数据库相关位置进行了修改 : 

然后查看redis中相应数据没了 : 

然后再去网页端刷新界面  : 

发现数据已经修改 : 

6 . 缓存穿透问题解决思路 : 

原理参考 : Redis -- 缓存穿透问题解决思路-CSDN博客

解决缓存穿透逻辑修改 : 

这里采用缓存空对象 + 设置过期时间的方法来操作 ;

首先修改查询逻辑:

当缓存未命中并且数据库中没有相应数据的时候,将空值存入redis中,设置2min的过期时间 : 

如果命中空的,直接返回一个错误信息 : 

完整代码 : 


    @Resource
    private StringRedisTemplate stringRedisTemplate ;

    @Override
    public Result queryById(Long id) { // 根据id查商户
        String key = CACHE_SHOP_KEY + id ;
        // 1 . 从redis中查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2 . 判断是否存在
        if(StrUtil.isNotBlank(shopJson)) {
            // 3 . 存在 , 根据id查询数据库
            Shop shop = JSONUtil.toBean(shopJson,Shop.class) ;
            return Result.ok(shop) ;
        }
        // 判断命中的是否是空值
        if(shopJson != null){ // 一定是空字符串 , 前面isNotBlnak只有当shopJon存在且非空的情况下返回true
            // 命中空值
            return Result.fail("店铺不存在") ;
        }
        // 4 . 不存在 , 根据id查询数据库
        Shop shop = getById(id) ;
        // 4 .  不存在  , 返回错误
        if(shop==null){
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key , "" , CACHE_NULL_TTL,TimeUnit.MINUTES);
            // 返回错误信息
            return Result.fail("店铺不存在!") ;
        }
        // 6 . 存在 , 写入redis
        // 加入缓存过期时间
        stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

这里直接查询一个异常数据作为测试 : 

 也可以看到redis多了一个空的数据 : 

7 . Redis 缓存雪崩问题

原理参考 : Redis -- 缓存雪崩问题-CSDN博客

8 . 缓存击穿问题

原理参考 : Redis -- 缓存击穿问题-CSDN博客

问题引入 : 

常用解决方案 : 

  • 互斥锁 : 性能较差 , 后面来的线程等待时间可能过长 ;
  • 逻辑过期 : 逻辑上添加过期时间,不主动设置过期时间 ;

两种方案对比 : 

9 . 利用互斥锁解决缓存击穿问题

        核心思路:相较于原来从缓存中查询不到数据后直接查询数据库而言,现在的方案是 进行查询之后,如果从缓存没有查询到数据,则进行互斥锁的获取,获取互斥锁后,判断是否获得到了锁,如果没有获得到,则休眠,过一会再进行尝试,直到获取到锁为止,才能进行查询

       如果获取到了锁的线程,再去进行查询,查询后将数据写入redis,再释放锁,返回数据,利用互斥锁就能保证只有一个线程去执行操作数据库的逻辑,防止缓存击穿

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

这里的互斥锁采用redis中的setnx实现;

setnx的特点 : 

 当第一个人用setnx设置了lock,其它线程就不能够进行修改,保证了互斥的条件 ;

获取锁和释放锁的代码 : 

    private boolean tryLock(String key){
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key , "1",10,TimeUnit.SECONDS) ;
        return BooleanUtil.isTrue(flag) ;
    }

    private void unlock(String key){
        stringRedisTemplate.delete(key) ;
    }

注意在获取锁的时候,别直接return false, 因为在拆箱的过程中,可能会有空指针,造成异常 ;

用互斥锁实现缓存击穿完整代码 : 


    @Resource
    private StringRedisTemplate stringRedisTemplate ;

    @Override
    public Result queryById(Long id) throws InterruptedException { // 根据id查商户
        // 缓存穿透
        // Shop shop = queryWithPassThrough(id) ;

        // 互斥锁解决缓存击穿
        Shop shop = queryWithMutex(id) ;

        if(shop == null){
            return Result.fail("店铺不存在!") ;
        }

        return Result.ok(shop);
    }

    public Shop queryWithMutex(Long id) throws InterruptedException { // 缓存穿透代码
        String key = CACHE_SHOP_KEY + id ;
        // 1 . 从redis中查询商户缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2 . 判断是否存在
        if(StrUtil.isNotBlank(shopJson)) {
            // 3 . 存在 , 根据id查询数据库
            Shop shop = JSONUtil.toBean(shopJson,Shop.class) ;
            return shop ;
        }
        // 判断命中的是否是空值
        if(shopJson != null){ //上面isNotBlank方法判断""和null都是返回false,因为缓存空对象为"",所以判断不是null
            // 命中空值
//            return Result.fail("店铺不存在") ;
            return null ;
        }

        // 实现缓存重建
        // 4. 1 获取互斥锁
        String lockKey = "lock:shop:" + id ;
        Shop shop = null;
        try {
            boolean isLock = tryLock(lockKey) ;
            // 4 .2 判断是否获取成功
            if(!isLock){
                // 4 . 3 失败 , 则休眠并重试
                Thread.sleep(50) ;
                return queryWithMutex(id) ;
            }
            // 4 . 4 成功 , 根据id查询数据库
            // 模拟重建的超时
            Thread.sleep(200);
            shop = getById(id);
            // 5 .  不存在  , 返回错误
            if(shop==null){
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key , "" , CACHE_NULL_TTL,TimeUnit.MINUTES);
                // 返回错误信息
                // return Result.fail("店铺不存在!") ;
                return null ;
            }
            // 6 . 存在 , 写入redis
            // 加入缓存过期时间
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        } finally {
            // 7 . 释放互斥锁
            unlock(lockKey);
        }
        // 8 . 返回shop
        return shop;
    }

测试 :  

在重建的步骤中,加入休眠时间 : 

清空redis库 : 

这里本来要用Jmeter来测试,但是懒得搞,然后补上,学习网址 : Jmeter自动化测试工具从入门到进阶6小时搞定,适合手工测试同学学习_哔哩哔哩_bilibili

这里随便测一测 : 发现数据只查询了一次 : 

10 . 利用逻辑过期解决缓存击穿问题

这里key是不会过期的 ;

流程 :

先去定义一个类,专门定义逻辑过期时间 : 

其中data用来存放shop ;

首先进行数据准备 : 

    public void saveShop2Redis(Long id, Long expireSeconds){
        // 1 . 查询店铺数据
        Shop shop = getById(id) ;
        // 2 . 封装逻辑过期时间
        RedisData redisData = new RedisData() ;
        redisData.setData(shop);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(expireSeconds));
        // 3 . 写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id ,JSONUtil.toJsonStr(redisData));
    }

然后用单元测试的方式,来写入数据 , 进行热点key的数据预热 : 

完整代码 : 


    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10) ;
    public Shop queryWithLogicalExpire( Long id ) {
        String key = CACHE_SHOP_KEY + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 这里不需要考虑缓存穿透
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return shop;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            CACHE_REBUILD_EXECUTOR.submit( ()->{

                try{
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                }catch (Exception e){
                    throw new RuntimeException(e);
                }finally {
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return shop;
    }

数据库修改之后,发现redis中也修改了:

这里会有一段时间的不一致 ;

11 . 封装redis工具类

需求 : 

完整代码 (值得细细体会): 

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 org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

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.CACHE_NULL_TTL;
import static com.hmdp.utils.RedisConstants.LOCK_SHOP_KEY;

@Component
public class CacheClient {
    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);
    private StringRedisTemplate stringRedisTemplate ;


    private CacheClient(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate ;
    }

    /**
     *  将传过来的对象序列化为json并存储在string类型的key中,并且可以设置过期时间
     * @param key
     * @param value // 任意java对象
     * @param time // 过期时长
     * @param unit // 时间单位
     */
    public void set(String key , Object value , Long time , TimeUnit unit){
        stringRedisTemplate.opsForValue().set(key , JSONUtil.toJsonStr(value),time , unit);
    }

    /**
     *  将传过来的对象序列化为json并存储在string类型的key中,并且可以设置逻辑过期时间,用于处理缓存击穿问题
     * @param key
     * @param value // 任意java对象
     * @param time // 过期时长
     * @param unit // 时间单位
     */
    public void setWithLogicalExpire(String key , Object value , Long time , TimeUnit unit){
        // 设置RedisData对象
        RedisData redisData = new RedisData() ;
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        stringRedisTemplate.opsForValue().set(key , JSONUtil.toJsonStr(redisData));
    }


    public <R,ID> R queryWithPassThrough(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit){
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(json)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(json, type);
        }
        // 判断命中的是否是空值
        if (json != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.不存在,根据id查询数据库
        R r = dbFallback.apply(id);
        // 5.不存在,返回错误
        if (r == null) {
            // 将空值写入redis
            stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
            // 返回错误信息
            return null;
        }
        // 6.存在,写入redis
        this.set(key, r, time, unit);
        return r;
    }

    public <R, ID> R queryWithLogicalExpire(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String json = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isBlank(json)) {
            // 3.存在,直接返回
            return null;
        }
        // 4.命中,需要先把json反序列化为对象
        RedisData redisData = JSONUtil.toBean(json, RedisData.class);
        R r = JSONUtil.toBean((JSONObject) redisData.getData(), type);
        LocalDateTime expireTime = redisData.getExpireTime();
        // 5.判断是否过期
        if(expireTime.isAfter(LocalDateTime.now())) {
            // 5.1.未过期,直接返回店铺信息
            return r;
        }
        // 5.2.已过期,需要缓存重建
        // 6.缓存重建
        // 6.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        boolean isLock = tryLock(lockKey);
        // 6.2.判断是否获取锁成功
        if (isLock){
            // 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(() -> {
                try {
                    // 查询数据库
                    R newR = dbFallback.apply(id);
                    // 重建缓存
                    this.setWithLogicalExpire(key, newR, time, unit);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    // 释放锁
                    unlock(lockKey);
                }
            });
        }
        // 6.4.返回过期的商铺信息
        return r;
    }

    public <R, ID> R queryWithMutex(
            String keyPrefix, ID id, Class<R> type, Function<ID, R> dbFallback, Long time, TimeUnit unit) {
        String key = keyPrefix + id;
        // 1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        // 2.判断是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            // 3.存在,直接返回
            return JSONUtil.toBean(shopJson, type);
        }
        // 判断命中的是否是空值
        if (shopJson != null) {
            // 返回一个错误信息
            return null;
        }

        // 4.实现缓存重建
        // 4.1.获取互斥锁
        String lockKey = LOCK_SHOP_KEY + id;
        R r = null;
        try {
            boolean isLock = tryLock(lockKey);
            // 4.2.判断是否获取成功
            if (!isLock) {
                // 4.3.获取锁失败,休眠并重试
                Thread.sleep(50);
                return queryWithMutex(keyPrefix, id, type, dbFallback, time, unit);
            }
            // 4.4.获取锁成功,根据id查询数据库
            r = dbFallback.apply(id);
            // 5.不存在,返回错误
            if (r == null) {
                // 将空值写入redis
                stringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                // 返回错误信息
                return null;
            }
            // 6.存在,写入redis
            this.set(key, r, time, unit);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }finally {
            // 7.释放锁
            unlock(lockKey);
        }
        // 8.返回
        return r;
    }

    private boolean tryLock(String key) {
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.SECONDS);
        return BooleanUtil.isTrue(flag);
    }

    private void unlock(String key) {
        stringRedisTemplate.delete(key);
    }
    
}

  • 15
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 黑马qt公开课是一套为期5天的课件,主要介绍了Qt框架的基础知识和应用。Qt框架是一个跨平台的开发工具,可以方便地进行应用程序的设计、开发和调试,被广泛应用于图形界面开发、嵌入式系统和移动应用等领域。 在5天的课程中,学习者将对Qt框架的整体架构有一个全面的认识,包括Qt及其常用库的概念、功能和用法,也学会了如何使用Qt Designer进行界面设计和基于信号与槽的事件编程。 此外,课程还将介绍Qt中常用的编程模式和技术,如MVC架构、文件操作、网络编程等,并通过实例让学习者深入理解和应用这些概念和技术。 5天的课件中还提供了大量的实践操作,让学习者通过编写实际案例,深入理解所学知识,并更好地掌握Qt框架的基础和应用,为以后的工作打下坚实的基础。 总之,如果你想快速入门Qt框架的基础知识和应用,那么黑马qt公开课—5天的课件,将是一个非常好的选择。 ### 回答2: 黑马qt公开课的课件共分为5天,内容涵盖了Qt的基础知识、UI设计、绘图系统、多线程编程和网络编程等方面。通过这5天的学习,学员可以全面掌握Qt的开发技能和应用场景,具备开发Qt应用程序的能力。 第一天课程主要介绍了Qt的基础知识,包括Qt窗口和控件、信号与槽机制、事件处理、布局和样式等内容。通过这些基础知识的学习,学员可以了解Qt的基本工作原理和操作方法。 第二天的课程主要讲解了Qt的UI设计,包括UI设计器的使用、自定义控件和样式等内容。学员可以从中学习到如何设计美观、直观的用户界面。 第三天的课程则主题为Qt的绘图系统,包括2D和3D绘图、动画效果和图形转换等内容。在这一天的课程中,学员可以学习到如何使用Qt进行图形绘制和界面效果的优化。 第四天的课程主要介绍了Qt的多线程编程,包括线程的创建和管理、互斥锁和信号量等内容。学员可以从中学习到如何在Qt中实现多线程应用程序。 第五天的课程则主题为Qt的网络编程,包括socket编程、HTTP协议和Web服务等内容。学员可以从中学习到如何使用Qt进行网络编程,实现客户端和服务器的互通。 总体来说,黑马qt公开课的5天课程涵盖了Qt的核心知识点,让学员能够全面掌握Qt的开发技能和应用场景。通过这些课程的学习,学员可以成为一名合格的Qt开发工程师。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值