文章目錄
- 基于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