redis操作全是在本地虚拟机下进行,虚拟机安装,安装遇见问题可以参考以下链接内容

虚拟机安装解决linux虚拟机上网问题和xshell连接虚拟机VMware虚拟机中linux CentOS7上网联网,简单粗暴亲测有效redis安装

一:认识redis

1.NoSql

使用redisson 如何手动创建一个redistemplate redis创建表_linux


2.redis(远程词典服务器,键值型NoSql数据库)

使用redisson 如何手动创建一个redistemplate redis创建表_redis_02

二:redis客户端(用于操作redis)

三:redis命令

  1. redis数据结构
  2. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_06


  3. 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_07

  4. 通用命令
  5. 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_08


  6. 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_09


  7. 使用redisson 如何手动创建一个redistemplate redis创建表_linux_10


  8. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_11

  9. String类型(三种格式:字符串,int,float)
  10. 使用redisson 如何手动创建一个redistemplate redis创建表_linux_12


  11. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_13

  12. Key的层级格式
  13. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_14


  14. 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_15

  15. Hash类型
  16. 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_16

    使用redisson 如何手动创建一个redistemplate redis创建表_linux_17

  17. List类型
  18. 使用redisson 如何手动创建一个redistemplate redis创建表_linux_18


  19. 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_19


  20. 使用redisson 如何手动创建一个redistemplate redis创建表_linux_20

  21. Set类型
  22. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_21


  23. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_22

  24. SortedSet类型(特点:可排序,元素不可重复,查询速度快)
  25. 使用redisson 如何手动创建一个redistemplate redis创建表_redis_23

  26. eg:
  27. 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_24

使用redisson 如何手动创建一个redistemplate redis创建表_redis_25


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_26


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_27

四:redis的Java客户端(Jedis)

1.JedisPool连接池
为什么要使用JedisPool连接词呢?-----Jedis本身是线性不安全的,频繁的创建和销毁连接会有性能损耗。

连接池相关设置

使用redisson 如何手动创建一个redistemplate redis创建表_redis_28

2.SpringDataRedis(以下采用springboot框架)
A:快速入门

  • 引入依赖
  • 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_29

  • 配置Redis相关信息(spring默认连接池是lettuce,也可修改为redis,需要手动配置才可以生效)
  • 使用redisson 如何手动创建一个redistemplate redis创建表_linux_30

  • 注入RedisTemplate,编写代码
  • 使用redisson 如何手动创建一个redistemplate redis创建表_redis_31

B:SpringDataRedis的序列化与反序列化

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_32

a:方案一(缺点:占用内存,存对象时会存入对象的类标识,以实现自动反序列化)

redisTemplate的set方法采用的是jdk的序列化器----导致相同的key存入reidis时被序列化成不同字节

使用redisson 如何手动创建一个redistemplate redis创建表_redis_33


解决办法:重写RedisTemplate方法

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_34


b:方案二(缺点:比较麻烦,需要手动处理)

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_35

五:Redis实战 (黑马点评项目 )

1.基于Redis实现短信功能

  • 流程图
  • 代码实现
package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.LoginFormDTO;
import com.hmdp.dto.Result;
import com.hmdp.dto.UserDTO;
import com.hmdp.entity.User;
import com.hmdp.mapper.UserMapper;
import com.hmdp.service.IUserService;
import com.hmdp.utils.RegexUtils;
import com.hmdp.utils.SystemConstants;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.*;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        //1.校验手机号
       if(RegexUtils.isPhoneInvalid(phone)){
           //2.不符合,返回错误信息
           return Result.fail("手机号格式错误");
       }
       //3.符合,生成验证码(hutool-all依赖中的方法)
        String code = RandomUtil.randomString(6);
       //4.保存到session/redis
        //session.setAttribute("code",code);
        redisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL,TimeUnit.MINUTES);
        //5.发送验证码
        log.debug("发送验证码成功,验证码:{}",code);
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        String phone = loginForm.getPhone();
        //1.校验手机号
        if(RegexUtils.isPhoneInvalid(phone)){
            //2.失败,返回错误信息
            return Result.fail("手机号码格式错误");
        }
        //3.校验验证码(从session或redis中获取)
        //String cacheCode =(String) session.getAttribute("code");
        String cacheCode = redisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        if(cacheCode==null||!cacheCode.equals(loginForm.getCode())){
            //4.不一致,报错
            return Result.fail("验证码错误");
        }
        //5.查询用户根据手机号
        User user = query().eq("phone", phone).one();
        //6.判断用户是否存在
        if(user==null){
            //7.不存在,创建用户
           user = createUserByPhone(phone);
        }
        //8.保存用户到session/redis
        //session.setAttribute("user", BeanUtil.copyProperties(user, UserDTO.class));
        //8.1随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //8.2将user对象转为Hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> map = BeanUtil.beanToMap(userDTO,new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName,fieldValue)->fieldValue.toString()));
        //8.3存储
        String tokenKey=LOGIN_USER_KEY+token;
        redisTemplate.opsForHash().putAll(tokenKey,map);
        //8.4设置token有效期
        redisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        //9.返回token
        return Result.ok(token);
    }

    private User createUserByPhone(String phone){
        //1.创建用户
        User user=new User();
        user.setPhone(phone);
        user.setNickName(SystemConstants.USER_NICK_NAME_PREFIX +RandomUtil.randomString(10));
        //2.保存用户(mybatis-plus提供)
         save(user);
        return user;
    }
}

六.缓存(存于Redis中)

1. 缓存定义

使用redisson 如何手动创建一个redistemplate redis创建表_redis_36


2. 缓存实例

使用redisson 如何手动创建一个redistemplate redis创建表_linux_37

@Override
    public Result queryById(Long id) {
        String key=CACHE_SHOP_KEY + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.数据库不存在,返回错误
        if (shop==null){
            return Result.fail("店铺不存在!");
        }
        //7.返回
        return Result.ok(shop);
    }

3. 缓存更新策略

使用redisson 如何手动创建一个redistemplate redis创建表_redis_38


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_39

使用redisson 如何手动创建一个redistemplate redis创建表_linux_40

线程发生安全情况问题

使用redisson 如何手动创建一个redistemplate redis创建表_linux_41

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_42

//6.存在,存入redis,设置时间(CACHE_SHOP_TTL是时间30分钟)
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
@Override
    public Result update(Shop shop) {
        Long id = shop.getId();
        if(id==null){
            return Result.ok("店铺不能为空");
        }
        //1.更新数据库
        updateById(shop);
        //2.删除缓存
        stringRedisTemplate.delete(CACHE_SHOP_KEY +id);
        return Result.ok();
    }

4. 缓存穿透
指客户端请求的数据在缓存中和 数据库中都不存在,这样缓存永远不会有效,这些请求会达到数据库。
常见解决方案

  • 缓存空对象
    优点:实现简单,维护方便
    缺点:额外的内存消耗;
    可能造成短期的不一致(刚请求的id数据库不存在,此时数据库向改id插入数据,这样会导致查不到)
  • 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_43

  • 布隆过滤
    优点:内存占用少,没有多余key
    缺点:实现复杂,存在可能误判(不存在一定不存在)
  • 使用redisson 如何手动创建一个redistemplate redis创建表_redis_44

  • 实例:
  • 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_45

@Override
    public Result queryById(Long id) {
        String key=CACHE_SHOP_KEY + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        //判断命中的是否是空值
        if (shopJson!=null){
            //返回错误信息
            return Result.fail("店铺信息不存在!");
        }
        //4.不存在,根据id查询数据库
        Shop shop = getById(id);
        //5.数据库不存在,返回错误
        if (shop==null){
            //将空值写入redis
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            //发送错误信息
            return Result.fail("店铺不存在!");
        }
        //7.返回
        return Result.ok(shop);
    }

总结

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_46

5. 缓存雪崩

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

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_47

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

  • 常见解决方案:
  • 互斥锁

案列

---------------------

使用redisson 如何手动创建一个redistemplate redis创建表_redis_48

  • 采用String数据结构中的setnx命令(已存在的key不可以更新值,只有删除才可)
private boolean getLock(String key){
        //开启锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        //直接返回会拆箱
        return BooleanUtil.isTrue(flag);
    }
//定义解锁
    private void unlock(String key){
        stringRedisTemplate.delete(key);
    }
//缓存击穿互斥锁
    public Shop queryWithMutex(Long id){
        String key=CACHE_SHOP_KEY + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isNotBlank(shopJson)){
            //3.存在,返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        //判断命中的是否是空值(解决穿透)
        if (shopJson!=null){
            //返回错误信息
            return  null;
        }
        //4.未命中,实现缓存重建
        //4.1.获取互斥锁
        String lockKey=LOCK_SHOP_KEY+id;
        Shop shop = null;
        try {
            boolean islock = getLock(lockKey);
            //4.2.判断是否获取成功
            if (!islock){
                //4.3.失败,则休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //4.4.成功,根据id查询数据库
            shop = getById(id);
            //模拟重建延时,因为我们是本地查询数据库,速度快
            Thread.sleep(200);
            //5.数据库不存在,写空值(解决缓存穿透)
            if (shop==null){
                //将空值写入redis
                stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
                //发送错误信息
                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.返回
        return shop;
    }
  • 逻辑过期

案例

-------------

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_49

在service层实现逻辑过期处理换成击穿

//定义线程池完成
    private static final ExecutorService CACHE_REBUILD_EXECUTOR= Executors.newFixedThreadPool(10);

    //逻辑过期解决缓存击穿
    public Shop queryWithLogicalExpire(Long id){
        String key=CACHE_SHOP_KEY + id;
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2.判断是否存在
        if(StrUtil.isBlank(shopJson)){
            //3.不存在,返回
            return null;
        }
        //4.存在,判断过期时间,json反序列化为对象
        RedisData redisData = JSONUtil.toBean(shopJson, 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 = getLock(lockKey);
        //6.2.判断是否获取成功
        if (islock){
            // TODO 6.3.成功,开启独立线程,实现缓存重建
            CACHE_REBUILD_EXECUTOR.submit(()->{
                try {
                    //重建缓存
                    this.saveShop2Redis(id,20L);
                } catch (Exception e) {
                    throw new RuntimeException(e);
                }finally {
                    //释放锁
                    unlock(lockKey);
                }
            });
        }
        //6.4.返回过期的商铺信息
        return shop;
    }

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_50

两种解决方法对比

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_51

## 7.封装redis工具类

案例

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_52

```java
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 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;

@Slf4j
@Component
public class CacheClient {

    private final StringRedisTemplate stringRedisTemplate;

    private static final ExecutorService CACHE_REBUILD_EXECUTOR = Executors.newFixedThreadPool(10);

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

    //将数据存入Reis
    public void set(String key, Object value, Long time, TimeUnit unit) {
        stringRedisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(value), time, unit);
    }

    public void setWithLogicalExpire(String key, Object value, Long time, TimeUnit unit) {
        // 设置逻辑过期
        RedisData redisData = new RedisData();
        redisData.setData(value);
        redisData.setExpireTime(LocalDateTime.now().plusSeconds(unit.toSeconds(time)));
        // 写入Redis
        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);
    }
}

七.基于Redis实现优惠卷秒杀功能

  1. 优惠卷的id不可重复,必须唯一,采取Redis中String数据结构incr来构成id

全局id必须具有特性

高可用:必须一直可用,不可能我需要的时候挂掉了

高性能:生成id必须够快

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_53


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_54

使用redisson 如何手动创建一个redistemplate redis创建表_redis_55

Redis实现全局id

package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;

@Component
public class RedisIdWorker {
    /**
     * 开始时间戳
     */
    private static final long BEGIN_TIMESTAMP = 1640995200L;
    /**
     * 序列号的位数
     */
    private static final int COUNT_BITS = 32;

    private StringRedisTemplate stringRedisTemplate;

    public RedisIdWorker(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public long nextId(String keyPrefix) {
        // 1.生成时间戳
        //1.1.得到当前时间
        LocalDateTime now = LocalDateTime.now();
        //1.2获得当前时间的秒数,括号内是时区
        long nowSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timestamp = nowSecond - BEGIN_TIMESTAMP;

        // 2.生成序列号
        // 2.1.获取当前日期,精确到天
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        // 2.2.自增长
        long count = stringRedisTemplate.opsForValue().increment("icr:" + keyPrefix + ":" + date);

        // 3.拼接并返回(时间戳在右侧,需要位移32位,拼接序列号)
        return timestamp << COUNT_BITS | count;
    }
}

2. 实现秒杀功能

实现流程图

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_56

@Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //1.根据id查询优惠卷
        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 isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId).update();
        if(!isSuccess){
            return Result.fail("库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3.代金卷id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

上述秒杀会出现超卖问题,超卖产生原因如下,多个线程同时进入查询数据库,线程1查到了数据还没执行,线程2也去查询数据,因此线程1,2查到的数据一样,然后又同时减去库存,导致超卖

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_57

3.处理上述秒杀超卖问题(加锁处理)

使用redisson 如何手动创建一个redistemplate redis创建表_linux_58

乐观锁

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_59

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_60

@Override
    @Transactional
    public Result seckillVoucher(Long voucherId) {
        //1.根据id查询优惠卷
        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 isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock",0).update();
        if(!isSuccess){
            return Result.fail("库存不足");
        }
        //6.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //6.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //6.2.用户id
        Long userId = UserHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //6.3.代金卷id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

4.秒杀实现一人一单

先晒流程

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_61

public Result seckillVoucher(Long voucherId) {
        //1.根据id查询优惠卷
        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一人一单
        Long userId = UserHolder.getUser().getId();
        //5.1查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在
        if (count>0){
            //用户购买过
            return Result.fail("用户已经购买了一次!");
        }
        //6.扣减库存(处理前语句)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId).update();
        //6.扣减库存(处理后语句)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock",0).update();
        if(!isSuccess){
            return Result.fail("库存不足");
        }

        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //7.2.用户id
        voucherOrder.setUserId(userId);
        //7.3.代金卷id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

上述代码块会出现并发线程不安全情况

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_62

解决上述办法就是加锁(悲观锁)

public Result seckillVoucher(Long voucherId) {
        //1.根据id查询优惠卷
        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();
        //悲观锁(放在这的原因:要等执行完事务并提交才释放锁,下一个线程才可以进行,
        // 放在其他地方会导致事务没提交,下一个线程就进来了)
        synchronized (userId.toString().intern()) {
            //spring执行事务,要获取代理对象,这里是获取代理对象
            IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
            return proxy.CreatVoucherOrder(voucherId);
        }
    }

    @Transactional
    public Result CreatVoucherOrder(Long voucherId){
        //5一人一单
        Long userId = UserHolder.getUser().getId();

        //5.1查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在
        if (count > 0) {
            //用户购买过
            return Result.fail("用户已经购买了一次!");
        }
        //6.扣减库存(处理前语句)
        /*boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId).update();*/
        //6.扣减库存(处理后语句)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!isSuccess) {
            return Result.fail("库存不足");
        }

        //7.创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //7.1.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //7.2.用户id
        voucherOrder.setUserId(userId);
        //7.3.代金卷id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }

5.上述处理方式适合单体系统,集群下行不通

>原因: 每一个端口都会是一个全新的JVM,导致锁监视器是全新的,所以锁不起效


解决上述问题采用如下方法(分布式锁)

使用redisson 如何手动创建一个redistemplate redis创建表_linux_63

分布式锁基本特点及实现的多种方式

使用redisson 如何手动创建一个redistemplate redis创建表_redis_64


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_65

6.基于Redis实现分布锁

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_66

JDK提供的锁有两种方式:1:阻塞模式(没找到锁,等待直到有人释放锁);2.非阻塞模式(没找到锁,就返回信息)

实现代码

  • 创建接口
package com.hmdp.utils;

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超市时间,过期自动释放
     * @return
     */
    boolean tryLock(long timeoutSec);

    /**
     * 释放锁
     */
    void unlock();
}
  • 实现接口
package com.hmdp.utils;

import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    /**
     * name 锁的key
     */
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private  static final String KEY_PREFIX="lock:";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程id
        long id = Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        stringRedisTemplate.delete(KEY_PREFIX+name);
    }
}
  • 调用方法(基于优惠卷秒杀一人一单)
public Result seckillVoucher(Long voucherId) {
        //1.根据id查询优惠卷
        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 {
            //spring执行事务,要获取代理对象,这里是获取代理对象
            IVoucherOrderService proxy =(IVoucherOrderService) AopContext.currentProxy();
            return proxy.CreatVoucherOrder(voucherId);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

`

上述代码存在误删锁的问题(改进如下)

使用redisson 如何手动创建一个redistemplate redis创建表_linux_67

流程如下

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_68

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.util.concurrent.TimeUnit;

public class SimpleRedisLock implements ILock{

    /**
     * name 锁的key
     */
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private  static final String KEY_PREFIX="lock:";
    private static final  String ID_PREFIX= UUID.randomUUID().toString(true)+"-";

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程id
        String id = ID_PREFIX+Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //获取线程id
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取锁中的标时
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断标识是否一致
        if (threadId.equals(id)){
            //释放锁
            stringRedisTemplate.delete(KEY_PREFIX+name);
        }

    }
}

上述问题解决了之后还可能存在延迟删锁问题

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_69

采用lua脚本语言解决

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_70


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_71


使用redisson 如何手动创建一个redistemplate redis创建表_缓存_72


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_73

lua脚本

-- 比较线程标示与锁中的标示是否一致
if(redis.call('get', KEYS[1]) ==  ARGV[1]) then
    -- 释放锁 del key
    return redis.call('del', KEYS[1])
end
return 0

修改锁接口实现类的释放锁方法

public class SimpleRedisLock implements ILock{

    /**
     * name 锁的key
     */
    private String name;
    private StringRedisTemplate stringRedisTemplate;
    private  static final String KEY_PREFIX="lock:";
    private static final  String ID_PREFIX= UUID.randomUUID().toString(true)+"-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT=new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    }

    public SimpleRedisLock(String name, StringRedisTemplate stringRedisTemplate) {
        this.name = name;
        this.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程id
        String id = ID_PREFIX+Thread.currentThread().getId();
        //获取锁
        Boolean success = stringRedisTemplate.opsForValue()
                .setIfAbsent(KEY_PREFIX+name, id+"", timeoutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //调用lua脚本
        stringRedisTemplate.execute(UNLOCK_SCRIPT, Collections.singletonList(KEY_PREFIX+name),
                ID_PREFIX+Thread.currentThread().getId());
    }
}

分布式锁总结

使用redisson 如何手动创建一个redistemplate redis创建表_linux_74

**

七:六中实现Redis分布式锁的优化

1.上述分布式锁存在问题

使用redisson 如何手动创建一个redistemplate redis创建表_linux_75

2.采用Redisson框架解决

使用redisson 如何手动创建一个redistemplate redis创建表_linux_76

3.Redisson入门

  • 导入依赖
<dependency>
            <groupId>org.redisson</groupId>
            <artifactId>redisson</artifactId>
            <version>3.13.6</version>
           </dependency>
  • 编写配置
package com.hmdp.config;

import org.redisson.Redisson;
import org.redisson.api.RedissonClient;
import org.redisson.config.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RedissonConfig {

    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.242.128:6379").setPassword("123321");
        //创建Redisson对象
        return Redisson.create(config);
    }
}
  • 使用
@Test
    void redissonClient() throws InterruptedException {
        //获取锁(可重入),指定锁的名称
        RLock lock = redissonClient.getLock("order");
        //尝试获取锁,参数是:获取锁的最大等待时间(期间会重复),锁自动释放时间,时间单位
        boolean isLock = lock.tryLock(1, 10, TimeUnit.SECONDS);
        //判断是否获取成功
        if(isLock){
            //防止执行事务发送异常
            try {
                System.out.println("succeed!");
            } finally {
                //释放锁
                lock.unlock();
            }
        }
    }

4.Redisson可重入原理(采用hash数据结构,每次获取锁判断是否是同一个用户,是的话value加1,释放锁value减1)

-执行的lua脚本

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_77

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_78

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_79

5.Redis的锁重试和watchDog(看门狗:(不设置锁释放时间启用)分布式锁是支持锁自动续期的,也就是说,如果线程仍旧没有执行完,那么redisson会自动给redis中的目标key延长超时时间)原理

使用redisson 如何手动创建一个redistemplate redis创建表_linux_80


使用redisson 如何手动创建一个redistemplate redis创建表_redis_81

6.Redisson解决主从一致)问题(多个redis节点,一台为主节点,主节点处理写操作,从节点处理读操作,主从节点要同步,有可能宕机,同步失败),MultiLock解决原理

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_82


7.总结


使用redisson 如何手动创建一个redistemplate redis创建表_redis_83


**

八.阻塞队列实现秒杀业务优化,提升性能

1.原秒杀架构

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_84

2.为了提高性能现改进如下

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_85

3.秒杀功能流程(开启新的线程池操作数据库,实现异步下单)

使用redisson 如何手动创建一个redistemplate redis创建表_linux_86

  • 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.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 2
end
-- 3.4.扣库存 incrby stockKey -1
redis.call('incrby', stockKey, -1)
-- 3.5.下单(保存用户)sadd orderKey userId
redis.call('sadd', orderKey, userId)
return 0
  • 业务代码
package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redisson;
    //加载lua脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT=new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    private IVoucherOrderService proxy;
    //阻塞队列,()是大小
    private BlockingQueue<VoucherOrder> orderTasks=new ArrayBlockingQueue<>(1024*1024);
    //线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
    }

    private class  VoucherOrderHandle implements Runnable{

        @Override
        public void run() {
            while(true){
                try {
                    //1.获取队列中订单信息
                    VoucherOrder take = orderTasks.take();
                    //2.创建订单
                    handleVoucherOrder(take);
                } catch (Exception e) {
                   log.error("处理订单异常",e);
                }
            }
        }
    }

    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //1.获取用户id
        Long userId = voucherOrder.getUserId();
        //创建锁对象
        RLock lock = redisson.getLock("order:" + userId);
        //获取锁,到时间释放
        boolean isLock = lock.tryLock();
        //判断是否成功
        if (!isLock){
            //失败
            log.error("不允许重复下单");
            return ;
        }
        try {
             proxy.CreatVoucherOrder(voucherOrder);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 优化秒杀功能后
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        Long userId = UserHolder.getUser().getId();
        //1.执行lua脚本
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(), voucherId.toString(), userId.toString());
        //2.判断结果是否为0
        if (result.intValue()!=0){
            //2.1不为0,代表没资格购买
            return Result.fail(result.intValue()==1?"库存不足":"不能重复下单");
        }
        //TODO 保存阻塞队列
        //2.2.为0,有购买资格,把下单信息保存到阻塞队列
        VoucherOrder voucherOrder = new VoucherOrder();
        //2.3.订单id
        long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //2.4.用户id
        voucherOrder.setUserId(userId);
        //2.5.代金卷id
        voucherOrder.setVoucherId(voucherId);
        //2.6.放入阻塞队列
        orderTasks.add(voucherOrder);
        //3.获取代理对象
        proxy =(IVoucherOrderService) AopContext.currentProxy();
        //4.返回订单id
        return Result.ok(orderId);
    }


    @Transactional
    public void CreatVoucherOrder(VoucherOrder voucherOrder){
        //5一人一单
        Long userId = UserHolder.getUser().getId();
        Long voucherId = voucherOrder.getVoucherId();
        //5.1查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在
        if (count > 0) {
            //用户购买过
            log.error("不能重复购买");
        }
        //6.扣减库存(处理后语句)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!isSuccess) {
            log.error("库存不足");
        }

        save(voucherOrder);
    }
}

4.总结(队列用的是jvm的内存,不限制会导致内存溢出)

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_87

九.消息队列实现异步秒杀,提升性能

1. 消息队列是JVM以为的独立服务,不受JVM内存限制
2. 消息队列确保消息投递以后,消费者确认后才可以销毁数据

Redis实现消息队列的方式

  • List结构:基于list结构模拟消息队列
  • PubSub:基于点对点消息模型
  • Stream:比较完善的消息队列模型

> 消息队列

- 基于List的消息队列

使用redisson 如何手动创建一个redistemplate redis创建表_linux_88


使用redisson 如何手动创建一个redistemplate redis创建表_linux_89

使用redisson 如何手动创建一个redistemplate redis创建表_redis_90

- 基于PubSub的消息队列

使用redisson 如何手动创建一个redistemplate redis创建表_redis_91

  • 不存储数据,没有持久化,消息没人接收直接丢失,消费者接收到信息会缓存下来,超出空间会丢失。

使用redisson 如何手动创建一个redistemplate redis创建表_linux_92

- 基于Stream的消息队列–单消费者模式(消息一直存在,可供多消费者获取)

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_93


使用redisson 如何手动创建一个redistemplate redis创建表_缓存_94


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_95


使用redisson 如何手动创建一个redistemplate redis创建表_缓存_96

- 基于Stream的消息队列–消费组模式

使用redisson 如何手动创建一个redistemplate redis创建表_redis_97

使用redisson 如何手动创建一个redistemplate redis创建表_linux_98


使用redisson 如何手动创建一个redistemplate redis创建表_数据库_99


使用redisson 如何手动创建一个redistemplate redis创建表_缓存_100

基于Stream数据结构实现消息队列-----消费组

  • 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.库存不足,返回1
    return 1
end
-- 3.2.判断用户是否下单 SISMEMBER orderKey userId
if(redis.call('sismember', orderKey, userId) == 1) then
    -- 3.3.存在,说明是重复下单,返回2
    return 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
  • 实现代码
package com.hmdp.service.impl;

import cn.hutool.core.bean.BeanUtil;
import com.hmdp.dto.Result;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.aop.framework.AopContext;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.connection.stream.*;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Slf4j
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    @Resource
    private RedissonClient redisson;
    //加载lua脚本
    private static final DefaultRedisScript<Long> SECKILL_SCRIPT;
    static {
        SECKILL_SCRIPT=new DefaultRedisScript<>();
        SECKILL_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        SECKILL_SCRIPT.setResultType(Long.class);
    }
    private IVoucherOrderService proxy;

    //线程池
    private static final ExecutorService SECKILL_ORDER_EXECUTOR= Executors.newSingleThreadExecutor();

    @PostConstruct
    private void init(){
        SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandle());
    }
    String queueName="stream.orders";

    private class  VoucherOrderHandle implements Runnable{
        @Override
        public void run() {
            while(true){
                try {
                    //1.获取消息队列中订单信息 XREADGROUP GROUOP g1 c1 count 1 block 2000 STREAMS stream.orders >
                    // (g1组中c1从stream.orders读取1个数据,阻塞2s)
                    List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                            Consumer.from("g1", "c1"),
                            StreamReadOptions.empty().count(1).block(Duration.ofSeconds(2)),
                            StreamOffset.create(queueName, ReadOffset.lastConsumed())
                    );
                    //2.判断消息是否获取成功
                    if(list==null||list.isEmpty()){
                        //2.1.失败,没消息,继续下一次循环
                        continue;
                    }
                    //3.解析list中信息
                    MapRecord<String, Object, Object> record = list.get(0);
                    Map<Object, Object> value = record.getValue();
                    VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                    //4.成功,下单
                    handleVoucherOrder(voucherOrder);
                    //5.ack确认(从pending-list中除掉)
                    stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
                } catch (Exception e) {
                    log.error("处理订单异常",e);
                    handlePendingList();
                }
            }
        }
    }

    private void handlePendingList() {
        while(true){
            try {
                //1.获取pending-list中订单信息 XREADGROUP GROUOP g1 c1 count 1 STREAMS stream.orders 0
                // ()
                List<MapRecord<String, Object, Object>> list = stringRedisTemplate.opsForStream().read(
                        Consumer.from("g1", "c1"),
                        StreamReadOptions.empty().count(1),
                        StreamOffset.create(queueName,ReadOffset.from("0"))
                );
                //2.判断消息是否获取成功
                if(list==null||list.isEmpty()){
                    //2.1.失败,pending-list没消息,结束循环
                    break;
                }
                //3.解析list中信息
                MapRecord<String, Object, Object> record = list.get(0);
                Map<Object, Object> value = record.getValue();
                VoucherOrder voucherOrder = BeanUtil.fillBeanWithMap(value, new VoucherOrder(), true);
                //4.成功,下单
                handleVoucherOrder(voucherOrder);
                //5.ack确认(从pending-list中除掉)
                stringRedisTemplate.opsForStream().acknowledge(queueName,"g1",record.getId());
            } catch (Exception e) {
                log.error("处理pending-list订单异常",e);
            }
        }
    }



    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        //1.获取用户id
        Long userId = voucherOrder.getUserId();
        //创建锁对象
        RLock lock = redisson.getLock("order:" + userId);
        //获取锁,到时间释放
        boolean isLock = lock.tryLock();
        //判断是否成功
        if (!isLock){
            //失败
            log.error("不允许重复下单");
            return ;
        }
        try {
             proxy.CreatVoucherOrder(voucherOrder);
        } finally {
            //释放锁
            lock.unlock();
        }
    }

    /**
     * 优化秒杀功能后(Stream消息队列--群消费)
     * @param voucherId
     * @return
     */
    @Override
    public Result seckillVoucher(Long voucherId) {
        //获取用户
        Long userId = UserHolder.getUser().getId();
        //获取订单id
        long orderId = redisIdWorker.nextId("order");
        //1.执行lua脚本
        Long result = stringRedisTemplate.execute(SECKILL_SCRIPT,
                Collections.emptyList(), voucherId.toString(), userId.toString(),String.valueOf(orderId));
        //2.判断结果是否为0
        int r = result.intValue();
        if (r!=0){
            //2.1不为0,代表没资格购买
            return Result.fail(r ==1?"库存不足":"不能重复下单");
        }
        //3.获取代理对象
        proxy =(IVoucherOrderService) AopContext.currentProxy();
        //4.返回订单id
        return Result.ok(orderId);
    }

    @Transactional
    public void CreatVoucherOrder(VoucherOrder voucherOrder){
        //5一人一单
        Long userId = UserHolder.getUser().getId();
        Long voucherId = voucherOrder.getVoucherId();
        //5.1查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        //5.2判断是否存在
        if (count > 0) {
            //用户购买过
            log.error("不能重复购买");
        }
        //6.扣减库存(处理后语句)
        boolean isSuccess = seckillVoucherService.update().setSql("stock = stock-1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0).update();
        if (!isSuccess) {
            log.error("库存不足");
        }

        save(voucherOrder);
    }
}

**

十:实现达人探店功能

1.实现点赞功能(采用sortedset结构实现–点赞需要唯一,并且需要显示出前5名点赞的)

public 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.数据库点赞数+1
            boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
            //3.2.保存用户到redis
            if (isSuccess){
                stringRedisTemplate.opsForZSet().add(key,userId.toString(),System.currentTimeMillis());
            }
        }else {
            //4.已点赞,取消点赞
            //4.1.数据库点赞数-1
            boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();
            //4.2.删除用户从redis
            if (isSuccess){
                stringRedisTemplate.opsForZSet().remove(key,userId.toString());
            }
        }
        //5.返回
        return Result.ok();
    }

2.实现关注,取关(采用Redis中Set结构–关注需要唯一)

public Result follow(Long followUserId, Boolean isFollow) {
        //获取登录用户id
        Long userId = UserHolder.getUser().getId();
        //redis的key
        String key="follow"+userId;
        //1.判断是关注or取关
        if (isFollow){
            //2.关注
            Follow follow = new Follow();
            follow.setUserId(userId);
            follow.setFollowUserId(followUserId);
            boolean isSuccess = save(follow);
            if(isSuccess){
                //将关注用户id放入redis中set集合 sadd userId followerUserId

                stringRedisTemplate.opsForSet().add(key,followUserId.toString());
            }
        }else {
            //2.取关
            boolean isSuccess = remove(new QueryWrapper<Follow>().
                    eq("user_id", userId).
                    eq("follow_user_id", followUserId));
            if (isFollow){
                //把关注用户id移除从redis
                stringRedisTemplate.opsForSet().remove(key,followUserId.toString());
            }
        }
        return Result.ok();
    }

3.显示互相关注好友(求两个用户交集)

@Override
    public Result followCommonS(Long id) {
        //1.获取当前登录用户id
        Long userId = UserHolder.getUser().getId();
        String key="follow"+userId;
        //2.获取博主id
        String key2="follow"+id;
        //3.查询交集
        Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key, key2);
        if(intersect.isEmpty()||intersect==null){
            return Result.ok(Collections.emptyList());
        }
        //4.解析id
        List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());
        //5.查询用户
        List<UserDTO> userDTOS = userService.listByIds(ids)
                .stream()
                .map(user -> BeanUtil.copyProperties(user, UserDTO.class))
                .collect(Collectors.toList());
        return Result.ok(userDTOS);
    }

4.关注推送(采取feed流)

  • feed流模式
  • 使用redisson 如何手动创建一个redistemplate redis创建表_redis_101

  • 拉模式
  • 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_102

  • 推模式
  • 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_103

  • 推拉结合
  • 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_104

  • 总结
  • 使用redisson 如何手动创建一个redistemplate redis创建表_缓存_105

5.推模式实现推送,以及查询推送博文(博文查询采用feed流滚动分页查询),因此采用sortedset结构

  • 普通分页查询(通过下标查询,在新增数据情况下会出错)
  • 使用redisson 如何手动创建一个redistemplate redis创建表_数据库_106

  • 滚动分页查询采取的措施是对数据查询

使用redisson 如何手动创建一个redistemplate redis创建表_linux_107

public Result saveBlog(Blog blog) {
        //1.获取登录用户
        UserDTO user = UserHolder.getUser();
        blog.setUserId(user.getId());
        //2.保存探店博文
        boolean isSuccess = save(blog);
        //2.1.保存失败
        if(!isSuccess){
            return Result.fail("新增博文失败");
        }
        //2.2.成功,查询作者所有粉丝 select * from tb_follow where follow_user_id=?
        List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
        //3.推送笔记id给粉丝
        for (Follow follow:follows){
            //3.1.获取粉丝id
            Long userId = follow.getUserId();
            //4.2.推送
            String key=FEED_KEY+userId;
            stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
        }
        return Result.ok();
    }
public Result ofFollow(Long max,Integer offset) {
        String key=FEED_KEY+UserHolder.getUser().getId();
        //1.查询该用户的收件箱
        Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().
                reverseRangeByScoreWithScores(key, 0, max, offset, 2);
        //2.非空判断
        if (typedTuples.isEmpty()||typedTuples==null){
            return Result.ok();
        }
        //3.解析数据:blogId,minTime(该组中最小时间戳),offset(最小值相同个数)
        List<Long> ids = new ArrayList<>(typedTuples.size());
        long minTime=0;
        int count=1;
        for (ZSetOperations.TypedTuple<String> tuple:typedTuples){
            //3.1.获取blogId
            ids.add(Long.valueOf(tuple.getValue()));
            //3.2.获取分数(时间戳)
            Long time = tuple.getScore().longValue();
            if(time==minTime){
                count++;
            }else {
                minTime=time;
                count=1;
            }
        }
        //4.根据id查询blog
        String idStr = StrUtil.join(",", ids);
        List<Blog> blogs = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();

        for (Blog blog : blogs) {
           //4.1. 查询blog有关用户
            queryBlogUser(blog);
            //4.2.查询blog是否被点赞
            isBlogLiked(blog);
        }
        //5.封装数据,返回
        ScrollResult scrollResult = new ScrollResult();
        scrollResult.setList(blogs);
        scrollResult.setOffset(count);
        scrollResult.setMinTime(minTime);
        return Result.ok(scrollResult);
    }

**

**

十一:基于redis实现附近商户功能

1.GEO数据结构实现查找附近商铺

  • geoHash数据结构
  • 先导入数据坐标
@Test
    void importShopDate(){
        //1.查询店铺信息
        List<Shop> list = shopService.list();
        //第一种方法
        for (Shop shop : list) {
            String key = SHOP_GEO_KEY + shop.getTypeId();
            stringRedisTemplate.opsForGeo()
                    .add(key,new Point(shop.getX(), shop.getY()),shop.getId().toString());
        }
        
        //第二种
        /*//2.店铺分组,typeId一致的放一起
        Map<Long,List<Shop>> map=list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
        //3.分批导入
        for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
            //3.1.获取类型id
            Long typeId = entry.getKey();
            String key = SHOP_GEO_KEY + typeId;
            //3,2.获取同类型店铺
            List<Shop> value = entry.getValue();
            List<RedisGeoCommands.GeoLocation<String>> locations=new ArrayList<>();
            //3.3.写入redis GEOADD key 经度 纬度 member
            for (Shop shop : value) {
                locations.add(new RedisGeoCommands.GeoLocation<>(
                        shop.getId().toString(),
                        new Point(shop.getX(), shop.getY())));
            }
            stringRedisTemplate.opsForGeo()
                    .add(key,locations);*/
    }
  • 实现代码
public Result queryShopByType(Integer typeId, Integer current, Double x, Double y) {
        //1.判断是否需要根据坐标查询
        if(x==null||y==null){
            // 不需要坐标查询,按数据库查询
            Page<Shop> page = query().eq("type_id", typeId)
                    .page(new Page<>(current, SystemConstants.DEFAULT_PAGE_SIZE));
            // 返回数据
            return Result.ok(page);
        }
        String key=SHOP_GEO_KEY+typeId;
        //2.计算分页参数
        int form = (current-1)*SystemConstants.DEFAULT_PAGE_SIZE;
        int end=current*SystemConstants.DEFAULT_PAGE_SIZE;
        //3.查询redis,按照距离排序
        GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo()
                .search(key,
                        GeoReference.fromCoordinate(x, y),
                        new Distance(5000),
                        RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeDistance().limit(end)
                );
        //4.解析数据
        if(results==null){
            return Result.ok(Collections.emptyList());
        }
        List<GeoResult<RedisGeoCommands.GeoLocation<String>>> content = results.getContent();
        if(content.size()<=form){
            //没有数据了
            return Result.ok(Collections.emptyList());
        }
        //4.1.截取从from~end部分
        List<Long> ids=new ArrayList<>(content.size());
        Map<String,Distance> distanceMap=new HashMap<>(content.size());
        //采用stream流转换,skip是跳转到from,forEach开始循环
        content.stream().skip(form).forEach(result ->{
            //4.2.获取店铺id
            String shopIdStr = result.getContent().getName();
            ids.add(Long.valueOf(shopIdStr));
            //4.3.获取距离
            Distance distance = result.getDistance();
            distanceMap.put(shopIdStr,distance);
        });
        //5.根据id查询shop
        String idStr = StrUtil.join(",", ids);
        List<Shop> shops = query().in("id", ids).last("ORDER BY FIELD(id," + idStr + ")").list();
        for (Shop shop : shops) {
            shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
        }
        //6返回
        return Result.ok(shops);
    }

> 2.BitMap实现用户签到,签到统计功能

-BitMap介绍

使用redisson 如何手动创建一个redistemplate redis创建表_数据库_108


使用redisson 如何手动创建一个redistemplate redis创建表_缓存_109

  • 签到统计分析

使用redisson 如何手动创建一个redistemplate redis创建表_缓存_110

  • 实现代码(签到)
public Result sign() {
        //1.获取当前用户
        Long userId = UserHolder.getUser().getId();
        //2.获取日期
        LocalDateTime now = LocalDateTime.now();
        //3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        //4.获取今天是本月第几天
        int dayOfMonth = now.getDayOfMonth();
        //5.写入redis SETBIT KEY OFFSET ,返回true就是1
        redisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
        return Result.ok();
    }
  • 实现代码(签到统计)
public Result signCount() {
        //1.获取当前用户
        Long userId = UserHolder.getUser().getId();
        //2.获取日期
        LocalDateTime now = LocalDateTime.now();
        //3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = USER_SIGN_KEY + userId + keySuffix;
        //4.获取今天是本月第几天
        int dayOfMonth = now.getDayOfMonth();
        //5.获取本月截止今天为止的所有签到记录,返回的是十进制数字
        List<Long> result = redisTemplate.opsForValue().bitField(
                key,
                BitFieldSubCommands.create().get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
        );
        if(result==null||result.isEmpty()){
            return Result.ok(0);
        }
        // 取出返回结果(十进制数字)
        Long num = result.get(0);
        if(num==null||num==0){
            return Result.ok(0);
        }
        //6.处理数据,循环遍历数字与1进行与运算,判断是否签到(1为签到)
        int count=0;
        while(true){
            //6.1 与1做与运算,得到最后一位bit位 判断是否为0
            if ((num&1)==0){
                //为0,未签到,结束
                break;
            }else {
                //计数器+1
                count++;
            }
            //数字右移一位,抛弃最后一个bit位,获取下一个
            num >>>=1;
        }
        return Result.ok(count);
    }

3.HyperLogLog用法

使用redisson 如何手动创建一个redistemplate redis创建表_redis_111

使用redisson 如何手动创建一个redistemplate redis创建表_linux_112

  • 实例代码
void testHyperLogLog(){
        //定义数组
        String[] values=new String[1000];
        //插入1000000条数据
        for(int i=0 ;i<1000000;i++) {
            //数据处理,防止越界
            int j = i % 1000;
            values[j] = "user_" + i;
            //每1000条插入一次
            if (j == 999) {
                stringRedisTemplate.opsForHyperLogLog().add("hl2", values);
            }
        }
        //统计数量
        Long size = stringRedisTemplate.opsForHyperLogLog().size("hl2");
        System.out.println(size);
    }

**