Redis課程:黑馬點評

文章目錄

    • 基于Redis實現短信登錄
    • 商戶查詢緩存
    • 優惠券秒殺
      • 一人一單
    • 分布式鎖
      • Redis分布式鎖誤刪情況說明
      • 解決Redis分布式鎖誤刪問題
      • 使用lua腳本解決分布式鎖的原子性問題
    • 基于阻塞隊列實現秒殺優化
    • Redis消息隊列優化秒殺業務
    • 達人探店
    • 參考

本文是根據黑馬程序員的視頻課程 黑馬程序員Redis入門到實戰教程,深度透析redis底層原理+redis分布式鎖+企業解決方案+黑馬點評實戰項目整理而來。
模仿大眾點評的項目:

在這里插入圖片描述

基于Redis實現短信登錄

在這里插入圖片描述

代碼

    @Overridepublic Result login(LoginFormDTO loginForm, HttpSession session) {//1. 校驗手機號String phone = loginForm.getPhone();if (RegexUtils.isPhoneInvalid(phone)) {return Result.fail("手機號格式錯誤");}//2. 從redis獲取驗證碼并校驗String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);String code = loginForm.getCode();if (cacheCode == null || !cacheCode.equals(code)){//3. 不一致,報錯return Result.fail("驗證碼錯誤");}//4.一致,根據手機號查詢用戶User user = query().eq("phone", phone).one();//5. 判斷用戶是否存在if (user == null){//6. 不存在,創建新用戶user = createUserWithPhone(phone);}//7.保存用戶信息到redis// 7.1 隨機生成token,作為登錄令牌String token = UUID.randomUUID().toString(true);// 7.2 將User轉為HashMap存儲UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),CopyOptions.create().setIgnoreNullValue(true).setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));// 7.3 存儲String tokenKey = LOGIN_USER_KEY + token;stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);// 7.4 設置token有效期stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);// 8. 返回tokenreturn Result.ok(token);}

在這里插入圖片描述

商戶查詢緩存

在這里插入圖片描述

防止緩存穿透(在緩存和數據庫中都不存在的信息,多次查詢,會給數據庫帶來壓力),采用返回空值到redis的方案,下一次查詢直接顯示為空。還有一種方法是布隆過濾。

代碼如下:

@Resourceprivate StringRedisTemplate stringRedisTemplate;@Overridepublic Result queryById(Long id) {String key = CACHE_SHOP_KEY + id;// 1. 從redis查詢商戶緩存String shopJson = stringRedisTemplate.opsForValue().get(key);// 2. 判斷redis緩存中是否存在if (StrUtil.isNotBlank(shopJson)) {// 3. 存在,直接返回Shop shop = JSONUtil.toBean(shopJson, Shop.class); // Json數據轉換為java對象return Result.ok(shop);}// 判斷命中的是否是空值if (shopJson != null) {// 返回錯誤信息return  Result.fail("店鋪不存在!");}// 4. 不存在,根據id查詢數據庫Shop shop = getById(id);// 5. 數據庫中不存在,返回錯誤if (shop == null) {// 將空值寫入redisstringRedisTemplate.opsForValue().set(key, "", CACHE_NULL_TTL, TimeUnit.MINUTES);// 返回錯誤信息return Result.fail("店鋪不存在!");}// 6. 數據庫中存在,寫入redisstringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);// 7. 返回return Result.ok(shop);}

店鋪對應的數據存入redis中
在這里插入圖片描述

Redis和Mysql數據庫數據同步

根據id修改店鋪時,先修改數據庫,再刪除緩存:我們確定了采用刪除策略,來解決雙寫問題,當我們修改了數據之后,然后把緩存中的數據進行刪除,查詢時發現緩存中沒有數據,則會從mysql中加載最新的數據,從而避免數據庫和緩存不一致的問題

@Override@Transactionalpublic 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();}

如果你在redis中都找不到,就說明你查看的不是熱點數據啊,就直接返回你查看的熱點不存在就行了,這個是根據業務場景來實現的,跟普通的擊穿不一樣的

優惠券秒殺

在這里插入圖片描述

mysql數據庫中tb_voucher優惠券的表:
在這里插入圖片描述

實現優惠券秒殺的基本代碼:

 @Resourceprivate ISeckillVoucherService seckillVoucherService;@Resourceprivate RedisIdWorker redisIdWorker;@Override@Transactionalpublic Result seckillVoucher(Long voucherId) {// 1. 查詢優惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2. 判斷秒殺是否開始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未開始return Result.fail("秒殺尚未開始!");}// 3. 判斷秒殺是否結束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {return Result.fail("秒殺已經開始!");}// 4. 判斷庫存是否充足if (voucher.getStock() < 1) {// 庫存不足return Result.fail("庫存不足!");}// 5. 扣減庫存boolean success = seckillVoucherService.update().setSql("stock = stock - 1").eq("voucher_id", voucherId).update();if (!success) {// 庫存不足return Result.fail("庫存不足!");}// 6. 創建訂單VoucherOrder voucherOrder = new VoucherOrder();// 6.1 訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 6.2 用戶idLong userId = UserHolder.getUser().getId();voucherOrder.setUserId(userId);// 6.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 7. 返回訂單idreturn Result.ok(orderId);}

以上代碼存在一人可以領取多個優惠券的情形,下面實現一人一單的功能。

一人一單

@Transactionalpublic Result createVoucherOrder(Long voucherId) {// 5. 一人一單Long userId = UserHolder.getUser().getId();// 5.1 查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();// 5.2 判斷是否存在if (count > 0) {// 用戶已經購買過return Result.fail("用戶已經購買過一次!");}// 6. 扣減庫存boolean success = seckillVoucherService.update().setSql("stock = stock - 1")  // set stock = stock - 1.eq("voucher_id", voucherId).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 庫存不足return Result.fail("庫存不足!");}// 7. 創建訂單VoucherOrder voucherOrder = new VoucherOrder();// 7.1 訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 7.2 用戶idvoucherOrder.setUserId(userId);// 7.3 代金券idvoucherOrder.setVoucherId(voucherId);save(voucherOrder);// 8. 返回訂單idreturn Result.ok(orderId);}

同時加鎖,保證事務的特性,同時也控制了鎖的粒度。這樣可以解決單機情況下的一人一單安全問題,但是在集群模式下失效。

		Long userId = UserHolder.getUser().getId();synchronized (userId.toString().intern()) {// 獲取代理對象(事務)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);}

有關鎖失效原因分析

由于現在我們部署了多個tomcat,每個tomcat都有一個屬于自己的jvm,那么假設在服務器A的tomcat內部,有兩個線程,這兩個線程由于使用的是同一份代碼,那么他們的鎖對象是同一個,是可以實現互斥的,但是如果現在是服務器B的tomcat內部,又有兩個線程,但是他們的鎖對象寫的雖然和服務器A一樣,但是鎖對象卻不是同一個,所以線程3和線程4可以實現互斥,但是卻無法和線程1和線程2實現互斥,這就是 集群環境下,syn鎖失效的原因,在這種情況下,我們就需要使用分布式鎖來解決這個問題。

在這里插入圖片描述

分布式鎖

分布式鎖:滿足分布式系統或集群模式下多進程可見并且互斥的鎖。

分布式鎖的核心思想就是讓大家都使用同一把鎖,只要大家使用的是同一把鎖,那么我們就能鎖住線程,不讓線程進行,讓程序串行執行,這就是分布式鎖的核心思路

在這里插入圖片描述

基于Redis實現分布式鎖:redis作為分布式鎖是非常常見的一種使用方式,現在企業級開發中基本都使用redis或者zookeeper作為分布式鎖,利用setnx這個方法,如果插入key成功,則表示獲得到了鎖,如果有人插入成功,其他人插入失敗則表示無法獲得到鎖,利用這套邏輯來實現分布式鎖

實現分布式鎖時需要實現的兩個基本方法:

  • 獲取鎖:

    • 互斥:確保只能有一個線程獲取鎖
    • 非阻塞:嘗試一次,成功返回true,失敗返回false
  • 釋放鎖:

    • 手動釋放
    • 超時釋放:獲取鎖時添加一個超時時間

SimpleRedisLock

利用setnx方法進行加鎖,同時增加過期時間,防止死鎖,此方法可以保證加鎖和增加過期時間具有原子性

@Override
public boolean tryLock(long timeoutSec) {// 獲取線程標示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS); //SETNX 實現互斥效果return Boolean.TRUE.equals(success);
}
public void unlock() {//通過del刪除鎖stringRedisTemplate.delete(KEY_PREFIX + name);
}

修改業務代碼

  @Overridepublic Result seckillVoucher(Long voucherId) {// 1.查詢優惠券SeckillVoucher voucher = seckillVoucherService.getById(voucherId);// 2.判斷秒殺是否開始if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {// 尚未開始return Result.fail("秒殺尚未開始!");}// 3.判斷秒殺是否已經結束if (voucher.getEndTime().isBefore(LocalDateTime.now())) {// 尚未開始return Result.fail("秒殺已經結束!");}// 4.判斷庫存是否充足if (voucher.getStock() < 1) {// 庫存不足return Result.fail("庫存不足!");}Long userId = UserHolder.getUser().getId();//創建鎖對象(新增代碼)SimpleRedisLock lock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);//獲取鎖對象boolean isLock = lock.tryLock(1200);//加鎖失敗if (!isLock) {return Result.fail("不允許重復下單");}try {//獲取代理對象(事務)IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();return proxy.createVoucherOrder(voucherId);} finally {//釋放鎖lock.unlock();}}

Redis分布式鎖誤刪情況說明

持有鎖的線程在鎖的內部出現了阻塞,導致他的鎖自動釋放,這時其他線程,線程2來嘗試獲得鎖,就拿到了這把鎖,然后線程2在持有鎖執行過程中,線程1反應過來,繼續執行,而線程1執行過程中,走到了刪除鎖邏輯,此時就會把本應該屬于線程2的鎖進行刪除,這就是誤刪別人鎖的情況說明

解決方案:解決方案就是在每個線程釋放鎖的時候,去判斷一下當前這把鎖是否屬于自己,如果屬于自己,則不進行鎖的刪除,假設還是上邊的情況,線程1卡頓,鎖自動釋放,線程2進入到鎖的內部執行邏輯,此時線程1反應過來,然后刪除鎖,但是線程1,一看當前這把鎖不是屬于自己,于是不進行刪除鎖邏輯,當線程2走到刪除鎖邏輯時,如果沒有卡過自動釋放鎖的時間點,則判斷當前這把鎖是屬于自己的,于是刪除這把鎖。

解決Redis分布式鎖誤刪問題

需求:修改之前的分布式鎖實現,滿足:在獲取鎖時存入線程標示(用UUID + 線程id表示)
在釋放鎖時先獲取鎖中的線程標示,判斷是否與當前線程標示一致

  • 如果一致則釋放鎖
  • 如果不一致則不釋放鎖

核心邏輯:在存入鎖時,放入自己線程的標識,在刪除鎖時,判斷當前這把鎖的標識是不是自己存入的,如果是,則進行刪除,如果不是,則不進行刪除。

uuid用來區分jvm的,jvm內部用線程id區分

具體代碼如下:加鎖

private static final String ID_PREFIX = UUID.randomUUID().toString(true) + "-";
@Override
public boolean tryLock(long timeoutSec) {// 獲取線程標示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId, timeoutSec, TimeUnit.SECONDS);return Boolean.TRUE.equals(success);
}

釋放鎖

public void unlock() {// 獲取線程標示String threadId = ID_PREFIX + Thread.currentThread().getId();// 獲取鎖中的標示String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);// 判斷標示是否一致if(threadId.equals(id)) {// 釋放鎖stringRedisTemplate.delete(KEY_PREFIX + name);}
}

使用lua腳本解決分布式鎖的原子性問題

釋放鎖的lua腳本如下:unlock.lua

-- 這里的 KEYS[1] 就是鎖的key,這里的ARGV[1] 就是當前線程標示
-- 獲取鎖中的標示,判斷是否與當前線程標示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then-- 一致,則刪除鎖return redis.call('DEL', KEYS[1])
end
-- 不一致,則直接返回
return 0

我們的RedisTemplate中,可以利用execute方法去執行lua腳本

private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;static {UNLOCK_SCRIPT = new DefaultRedisScript<>();UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));UNLOCK_SCRIPT.setResultType(Long.class);}@Override
public void unlock() {// 調用lua腳本stringRedisTemplate.execute(UNLOCK_SCRIPT,Collections.singletonList(KEY_PREFIX + name),ID_PREFIX + Thread.currentThread().getId());
}

基于阻塞隊列實現秒殺優化

秒殺優化-異步秒殺思路

我們來回顧一下下單流程

當用戶發起請求,此時會請求nginx,nginx會訪問到tomcat,而tomcat中的程序,會進行串行操作,分成如下幾個步驟

1、查詢優惠卷

2、判斷秒殺庫存是否足夠

3、查詢訂單

4、校驗是否是一人一單

5、扣減庫存

6、創建訂單

在這六步操作中,又有很多操作是要去操作數據庫的,而且還是一個線程串行執行, 這樣就會導致我們的程序執行的很慢,所以我們需要異步程序執行,那么如何加速呢?

在這里插入圖片描述

需求:

  • 新增秒殺優惠券的同時,將優惠券信息保存到Redis中

  • 基于Lua腳本,判斷秒殺庫存、一人一單,決定用戶是否搶購成功

  • 如果搶購成功,將優惠券id和用戶id封裝后存入阻塞隊列

  • 開啟線程任務,不斷從阻塞隊列中獲取信息,實現異步下單功能

在這里插入圖片描述

秒殺優化-基于阻塞隊列實現秒殺優化

seckill.lua文件,實現上圖中的邏輯

-- 1.參數列表
-- 1.1.優惠券id
local voucherId = ARGV[1]
-- 1.2.用戶id
local userId = ARGV[2]-- 2.數據key
-- 2.1.庫存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.訂單key
local orderKey = 'seckill:order:' .. voucherId-- 3.腳本業務
-- 3.1.判斷庫存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.庫存不足,返回1return 1
end
-- 3.2.判斷用戶是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,說明是重復下單,返回2return 2
end
-- 3.4.扣庫存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下單(保存用戶)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0

VoucherOrderServiceImpl

修改下單動作,現在我們去下單時,是通過lua表達式去原子執行判斷邏輯,如果判斷我出來不為0 ,則要么是庫存不足,要么是重復下單,返回錯誤信息,如果是0,則把下單的邏輯保存到隊列中去,然后異步執行

//異步處理線程池
private static final ExecutorService SECKILL_ORDER_EXECUTOR = Executors.newSingleThreadExecutor();//在類初始化之后執行,因為當這個類初始化好了之后,隨時都是有可能要執行的
@PostConstruct
private void init() {SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}
// 用于線程池處理的任務
// 當初始化完畢后,就會去從隊列中去拿信息private class VoucherOrderHandler implements Runnable{@Overridepublic void run() {while (true){try {// 1.獲取隊列中的訂單信息VoucherOrder voucherOrder = orderTasks.take();// 2.創建訂單handleVoucherOrder(voucherOrder);} catch (Exception e) {log.error("處理訂單異常", e);}}}private void handleVoucherOrder(VoucherOrder voucherOrder) {//1.獲取用戶Long userId = voucherOrder.getUserId();// 2.創建鎖對象RLock redisLock = redissonClient.getLock("lock:order:" + userId);// 3.嘗試獲取鎖boolean isLock = redisLock.lock();// 4.判斷是否獲得鎖成功if (!isLock) {// 獲取鎖失敗,直接返回失敗或者重試log.error("不允許重復下單!");return;}try {//注意:由于是spring的事務是放在threadLocal中,此時的是多線程,事務會失效proxy.createVoucherOrder(voucherOrder);} finally {// 釋放鎖redisLock.unlock();}}private BlockingQueue<VoucherOrder> orderTasks =new  ArrayBlockingQueue<>(1024 * 1024);@Overridepublic Result seckillVoucher(Long voucherId) {Long userId = UserHolder.getUser().getId();long orderId = redisIdWorker.nextId("order");// 1.執行lua腳本Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,Collections.emptyList(),voucherId.toString(), userId.toString(), String.valueOf(orderId));int r = result.intValue();// 2.判斷結果是否為0if (r != 0) {// 2.1.不為0 ,代表沒有購買資格return Result.fail(r == 1 ? "庫存不足" : "不能重復下單");}VoucherOrder voucherOrder = new VoucherOrder();// 2.3.訂單idlong orderId = redisIdWorker.nextId("order");voucherOrder.setId(orderId);// 2.4.用戶idvoucherOrder.setUserId(userId);// 2.5.代金券idvoucherOrder.setVoucherId(voucherId);// 2.6.放入阻塞隊列orderTasks.add(voucherOrder);//3.獲取代理對象proxy = (IVoucherOrderService)AopContext.currentProxy();//4.返回訂單idreturn Result.ok(orderId);}@Transactionalpublic  void createVoucherOrder(VoucherOrder voucherOrder) {Long userId = voucherOrder.getUserId();// 5.1.查詢訂單int count = query().eq("user_id", userId).eq("voucher_id", voucherOrder.getVoucherId()).count();// 5.2.判斷是否存在if (count > 0) {// 用戶已經購買過了log.error("用戶已經購買過了");return ;}// 6.扣減庫存boolean success = seckillVoucherService.update().setSql("stock = stock - 1") // set stock = stock - 1.eq("voucher_id", voucherOrder.getVoucherId()).gt("stock", 0) // where id = ? and stock > 0.update();if (!success) {// 扣減失敗log.error("庫存不足");return ;}save(voucherOrder);}

秒殺業務的優化思路是什么?

  • 先利用Redis完成庫存余量、一人一單判斷,完成搶單業務
  • 再將下單業務放入阻塞隊列,利用獨立線程異步下單
  • 基于阻塞隊列的異步秒殺存在哪些問題?
    • 內存限制問題
    • 數據安全問題

Redis消息隊列優化秒殺業務

什么是消息隊列:字面意思就是存放消息的隊列。最簡單的消息隊列模型包括3個角色:

  • 消息隊列:存儲和管理消息,也被稱為消息代理(Message Broker)
  • 生產者:發送消息到消息隊列
  • 消費者:從消息隊列獲取消息并處理消息

基于Redis的Stream結構作為消息隊列,實現異步秒殺下單

需求:

  • 創建一個Stream類型的消息隊列,名為stream.orders
  • 修改之前的秒殺下單Lua腳本,在認定有搶購資格后,直接向stream.orders中添加消息,內容包含voucherId、userId、orderId
  • 項目啟動時,開啟一個線程任務,嘗試獲取stream.orders中的消息,完成下單
127.0.0.1:6379> XGROUP CREATE stream.orders g1 0 MKSTREAM
OK

在這里插入圖片描述

seckill.lua腳本中添加發送到消息隊列的內容

-- 1.參數列表
-- 1.1.優惠券id
local voucherId = ARGV[1]
-- 1.2.用戶id
local userId = ARGV[2]
-- 1.3.訂單id
local orderId = ARGV[3]-- 2.數據key
-- 2.1.庫存key
local stockKey = 'seckill:stock:' .. voucherId
-- 2.2.訂單key
local orderKey = 'seckill:order:' .. voucherId-- 3.腳本業務
-- 3.1.判斷庫存是否充足 get stockKey
if(tonumber(redis.call('get', stockKey)) <= 0) then-- 3.2.庫存不足,返回1return 1
end
-- 3.2.判斷用戶是否下單 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then-- 3.3.存在,說明是重復下單,返回2return 2
end
-- 3.4.扣庫存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下單(保存用戶)sadd orderKey userId
redis.call('sadd', orderKey, userId)
-- 3.6.發送消息到隊列中, XADD stream.orders * k1 v1 k2 v2 ...
redis.call('xadd', 'stream.orders', '*', 'userId', userId, 'voucherId', voucherId, 'id', orderId)
return 0

VoucherOrderServiceImpl

private class VoucherOrderHandler implements Runnable {@Overridepublic void run() {while (true) {try {// 1.獲取消息隊列中的訂單信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 >List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),StreamOffset.create("stream.orders", ReadOffset.lastConsumed()));// 2.判斷訂單信息是否為空if (list == null || list.isEmpty()) {// 如果為null,說明沒有消息,繼續下一次循環continue;}// 解析數據MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);// 3.創建訂單createVoucherOrder(voucherOrder);// 4.確認消息 XACKstringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());} catch (Exception e) {log.error("處理訂單異常", e);//處理異常消息handlePendingList();}}}private void handlePendingList() {while (true) {try {// 1.獲取pending-list中的訂單信息 XREADGROUP GROUP g1 c1 COUNT 1 BLOCK 2000 STREAMS s1 0List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(Consumer.from("g1", "c1"),StreamReadOptions.empty().count(1),StreamOffset.create("stream.orders", ReadOffset.from("0")));// 2.判斷訂單信息是否為空if (list == null || list.isEmpty()) {// 如果為null,說明沒有異常消息,結束循環break;}// 解析數據MapRecord<String, Object, Object> record = list.get(0);Map<Object, Object> value = record.getValue();VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);// 3.創建訂單createVoucherOrder(voucherOrder);// 4.確認消息 XACKstringRedisTemplate.opsForStream().acknowledge("s1", "g1", record.getId());} catch (Exception e) {log.error("處理pendding訂單異常", e);try{Thread.sleep(20);}catch(Exception e){e.printStackTrace();}}}}
}

達人探店

發布探店筆記

探店筆記類似點評網站的評價,往往是圖文結合。對應的表有兩個:
tb_blog:探店筆記表,包含筆記中的標題、文字、圖片等
tb_blog_comments:其他用戶對探店筆記的評價

在這里插入圖片描述

上傳接口

@Slf4j
@RestController
@RequestMapping("upload")
public class UploadController {@PostMapping("blog")public Result uploadImage(@RequestParam("file") MultipartFile image) {try {// 獲取原始文件名稱String originalFilename = image.getOriginalFilename();// 生成新文件名String fileName = createNewFileName(originalFilename);// 保存文件image.transferTo(new File(SystemConstants.IMAGE_UPLOAD_DIR, fileName));// 返回結果log.debug("文件上傳成功,{}", fileName);return Result.ok(fileName);} catch (IOException e) {throw new RuntimeException("文件上傳失敗", e);}}}

點贊功能

需求:

  • 同一個用戶只能點贊一次,再次點擊則取消點贊
  • 如果當前用戶已經點贊,則點贊按鈕高亮顯示(前端已實現,判斷字段Blog類的isLike屬性)

實現步驟:

  • 給Blog類中添加一個isLike字段,標示是否被當前用戶點贊
  • 修改點贊功能,利用Redis的set集合判斷是否點贊過,未點贊過則點贊數+1,已點贊過則點贊數-1
  • 修改根據id查詢Blog的業務,判斷當前登錄用戶是否點贊過,賦值給isLike字段
  • 修改分頁查詢Blog業務,判斷當前登錄用戶是否點贊過,賦值給isLike字段

在這里插入圖片描述

在探店筆記的詳情頁面,應該把給該筆記點贊的人顯示出來,比如最早點贊的TOP5,形成點贊排行榜:

之前的點贊是放到set集合,但是set集合是不能排序的,所以這個時候,咱們可以采用一個可以排序的set集合,就是咱們的sortedSet

具體步驟:

1、在Blog 添加一個字段

@TableField(exist = false)
private Boolean isLike;

2、修改代碼

   @Overridepublic Result likeBlog(Long id) {// 1.獲取登錄用戶Long userId = UserHolder.getUser().getId();// 2.判斷當前登錄用戶是否已經點贊String key = BLOG_LIKED_KEY + id;Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());if (score == null) {// 3.如果未點贊,可以點贊// 3.1.數據庫點贊數 + 1boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();// 3.2.保存用戶到Redis的set集合  zadd key value scoreif (isSuccess) {stringRedisTemplate.opsForZSet().add(key, userId.toString(), System.currentTimeMillis());}} else {// 4.如果已點贊,取消點贊// 4.1.數據庫點贊數 -1boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();// 4.2.把用戶從Redis的set集合移除if (isSuccess) {stringRedisTemplate.opsForZSet().remove(key, userId.toString());}}return Result.ok();}private void isBlogLiked(Blog blog) {// 1.獲取登錄用戶UserDTO user = UserHolder.getUser();if (user == null) {// 用戶未登錄,無需查詢是否點贊return;}Long userId = user.getId();// 2.判斷當前登錄用戶是否已經點贊String key = "blog:liked:" + blog.getId();Double score = stringRedisTemplate.opsForZSet().score(key, userId.toString());blog.setIsLike(score != null);}

BlogService

@Override
public Result queryBlogLikes(Long id) {String key = BLOG_LIKED_KEY + id;// 1.查詢top5的點贊用戶 zrange key 0 4Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);if (top5 == null || top5.isEmpty()) {return Result.ok(Collections.emptyList());}// 2.解析出其中的用戶idList<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());String idStr = StrUtil.join(",", ids);// 3.根據用戶id查詢用戶 WHERE id IN ( 5 , 1 ) ORDER BY FIELD(id, 5, 1)List<UserDTO> userDTOS = userService.query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list().stream().map(user -> BeanUtil.copyProperties(user, UserDTO.class)).collect(Collectors.toList());// 4.返回return Result.ok(userDTOS);
}

點贊排行榜顯示

在這里插入圖片描述

參考

[1] https://www.bilibili.com/video/BV1cr4y1671t?p=1&vd_source=c3b6e654ba39ea63bbf8fe47e7e98899

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/210675.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/210675.shtml
英文地址,請注明出處:http://en.pswp.cn/news/210675.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

vscode 開發c環境

前置條件&#xff1a; 1.vscode安裝C/C Extension Pack擴展 2.安裝gcc或者clang開發環境 在工程.vscode目錄下創建task任務文件 tasks.json {"tasks": [{"type": "cppbuild","label": "build","command": &q…

Kubernetes架構及核心部件

文章目錄 1、Kubernetes集群概述1.1、概述1.2、通過聲明式API即可 2、Kubernetes 集群架構2.1、Master 組件2.1.1、API Server2.1.2、集群狀態存儲2.1.3、控制器管理器2.1.4、調度器 2.2、Worker Node 組件2.2.1、kubelet2.2.2、容器運行時環境2.2.3、kube-proxy 2.3、圖解架構…

深入解析Node.js:V8引擎、事件驅動和非阻塞式I/O

文章目錄 1. 引言2. 什么是Node.js&#xff1f;3. V8引擎3.1 V8引擎簡介3.2 V8引擎的特點 4. 事件驅動4.1 事件循環4.2 事件觸發與監聽4.2.1 代碼示例 4.3 異步回調4.3.1 代碼示例 5. 非阻塞式I/O5.1 非阻塞式I/O的優勢5.2 異步與同步的對比5.2.1 同步I/O的代碼示例5.2.2 異步I…

前端知識筆記(三)———CSS核心功能手冊:從熟悉到精通

參考HTML代碼 <!DOCTYPE html> <html lang"zh-CN"><head><meta charset"utf-8"><meta http-equiv"X-UA-Compatible" content"IEedge"><meta name"viewport" content"widthdevice-wi…

指針 注意事項

指針變量的本質是用來放地址&#xff0c;而一般的變量是放數值的。 1、指針的定義&#xff08;兩種形式&#xff09; ①int x3; int *p&x ②int x; int *p&#xff1b; x3&#xff1b;p&x int*p中 *p和p的差別&#xff1a;簡單說*p是數值&#xff0c;p是地址&a…

Linux 系統上配置 SSH 密鑰

1. 生成 SSH 密鑰 打開終端&#xff0c;運行以下命令來生成 SSH 密鑰&#xff1a; ssh-keygen -t rsa -b 4096 -C "wqzbxhexample.com" 替換 "wqzbxhexample.com" 為你在 GitHub 注冊時使用的郵箱地址。 2. 添加 SSH 密鑰到 SSH 代理 運行以下命令來啟…

ROS gazebo 機器人仿真,環境與robot建模,添加相機 lidar,控制robot運動

b站上有一個非常好的ros教程234仿真之URDF_link標簽簡介-機器人系統仿真_嗶哩嗶哩_bilibili&#xff0c;推薦去看原視頻。 視頻教程的相關文檔見&#xff1a;6.7.1 機器人運動控制以及里程計信息顯示 Autolabor-ROS機器人入門課程《ROS理論與實踐》零基礎教程 本文對視頻教程…

java物聯網協議解析插件,java iot對接解析框架.java物聯網架構的設計思路

一般來說&#xff0c;物聯網開發則hi對硬件設備進行信息采集&#xff0c;所以建議技術棧如下&#xff1a; 物聯網開發技術棧一般如下&#xff1a; nettyspringbootrocketmqredismagic-byte 其中netty用于tcp和數據接入 rockemqt用于消息臨時儲存中轉&#xff0c; springboot就…

【論文精讀】REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS

REACT: SYNERGIZING REASONING AND ACTING IN LANGUAGE MODELS 前言ABSTRACT1 INTRODUCTION2 REACT: SYNERGIZING REASONING ACTING3 KNOWLEDGE-INTENSIVE REASONING TASKS3.1 SETUP3.2 METHODS3.3 RESULTS AND OBSERVATIONS 4 DECISION MAKING TASKS5 RELATED WORK6 CONCLUSI…

phpstudy搭建WordPress教程

一、phpstudy新建配置WordPress 打開phpstudy&#xff0c;啟動Apache&#xff08;或者Nginx&#xff09;和MySQL服務 來到數據庫部分&#xff0c;點擊[創建數據庫]&#xff0c;填寫新建數據庫的名稱&#xff0c;用戶名以及密碼&#xff0c;完成后點擊確認 來到網站部分&#x…

Course2-Week4-決策樹

Course2-Week4-決策樹 文章目錄 Course2-Week4-決策樹1. 決策樹的直觀理解2. 構建單個決策樹2.1 熵和信息增益2.2 構建決策樹——二元輸入特征2.3 構建決策樹——多元輸入特征2.4 構建決策樹——連續的輸入特征2.5 構建回歸樹——連續的輸出結果(選修)2.6 代碼實現-遞歸構建單個…

解決 php 連接mysql數據庫時報錯:Fatal error: Class ‘mysqli’ not found in問題

在使用php對mysql進行連接的過程中&#xff0c;出現了Fatal error: Uncaught Error: Class "mysqli" not found in的問題 解決方案 這個錯誤通常表示您的PHP代碼中缺少MySQL擴展或者沒有啟用MySQL擴展。 我們首先確認一下PHP環境中已經安裝了MySQL擴展。檢查一下自己…

Redis如何做內存優化?

Redis如何做內存優化&#xff1f; 1、縮短鍵值的長度 縮短值的長度才是關鍵&#xff0c;如果值是一個大的業務對象&#xff0c;可以將對象序列化成二進制數組&#xff1b; 首先應該在業務上進行精簡&#xff0c;去掉不必要的屬性&#xff0c;避免存儲一些沒用的數據&#xff1…

rust詳解

前言 rust 學習曲線非常陡峭&#xff0c;但是基本語法也還算挺好理解&#xff0c;自動內存管理有點類似智能指針&#xff0c;基本看一下語法入門就可以大概理解&#xff0c;但是唯獨宏很難理解&#xff0c;語法非常晦澀。但是功能非常強大。聲明宏類似于c語言的宏處理&#xf…

【淘寶網消費類電子產品銷售數據可視化】

淘寶網消費類電子產品銷售數據可視化 引言數據爬取與處理數據可視化系統功能1. 總數據量分析2. 店鋪總數據3. 店鋪銷售額排名4. 不同電子商品銷售價格5. 單個商品價格排名6. 不同省份平均銷量7. 不同地區的平均銷售額8. 省份數量9. 每個省份有用的平均個數 創新點結語 引言 隨…

Linux 中 find 查找

目錄 1.普通查詢 2.按照文件大小查找 3.忽略文件字母大小寫查詢 4.根據修改時間查找 5. 取反 &#xff01; 6.根據用戶查詢 7.對查找出來的內容進行操作 1.普通查詢 find 路徑 -name "文件名" 如查看 etc 目錄下的passwd 的文件 find /etc -name "passwd&quo…

【周報2023.12.09】

周報2023.12.09 本周開展工作下周工作計劃 本周開展工作 本周開展的工作的話一共是一下幾點&#xff1a; 這三點的話是緊密相連的 邏輯這邊需要考慮的東西很多 點擊生成照片&#xff0c;然后獲取生成照片的狀態點擊生成照片&#xff0c;然后獲取生成照片的時間&#xff0c;并…

kettle完成mysql表與表之間的更新和插入

版本&#xff1a;20231209 kettle完成數據庫表與表之間的轉換非常的簡單&#xff0c;只需要在輸入模塊選擇&#xff1a;輸入表&#xff1b;在輸出模塊選擇&#xff1a;插入和更新表模塊 實例展示&#xff1a;將表stu1的數據同步到stu2&#xff0c;并覆蓋掉stu2原本的數據。 cr…

嵌入式學習---ARM時鐘體系

目錄 時鐘相關概念時鐘脈沖時鐘頻率時鐘的作用時鐘信號的生成 S3C2440的時鐘體系主時鐘晶振兩個PLL 時鐘啟動流程相關的寄存器 時鐘相關概念 時鐘脈沖 按一定電壓幅度&#xff0c;一定時間間隔連續發出的脈沖信號。它是一個周期性的信號&#xff0c;每個周期內包含一個上升沿…

ConvNeXt V2: Co-designing and Scaling ConvNets with Masked Autoencoders

1.關于稀疏卷積的解釋&#xff1a;https://zhuanlan.zhihu.com/p/382365889 2. 答案&#xff1a; 在深度學習領域&#xff0c;尤其是計算機視覺任務中&#xff0c;遮蔽圖像建模&#xff08;Masked Image Modeling, MIM&#xff09;是一種自監督學習策略&#xff0c;其基本思想…