Redis学习记录

214 阅读24分钟

Redis的安装

Redis官网 redis.io/

最新版本的Redis已经到7.0了

image.png

这个是在centos7.6上的安装教程

1、首先确保需虚拟机上安装了 gcc , 可以使用以下的命令进行检查

gcc -v

image.png

我这没有安装,使用以下命令安装

yum install -y gcc

image.png

2、手动上传安装包 我这里上传到的是/usr/local目录下,并减压

cd /usr/local

tar -zxvf redis的压缩包名字

image.png

3、编译文件

进入刚才压缩出来的redis文件

执行make指令进行编译

4、安装到指定的目录

make install PREFIX=/usr/local/redis

image.png

image.png

5、修改配置文件

先拷贝一个redis的配置文件到/usr/local/redis/bin的目录下

image.png

修改为远程链接

image.png

修改成守护线程 image.png

设置密码 image.png

启动redis服务

image.png

6、设置开机自动启动

cd /lib/systemd/system/
# 新建文件
vim redis.service
[Unit]
Description=redis-server
After=network.target

[Service]
Type=forking
# ExecStart需要按照实际情况修改成自己的地址
ExecStart=/usr/local/redis/bin/redis-server /usr/local/redis/bin/redis.conf
PrivateTmp=true

[Install]
WantedBy=multi-user.target

设置开机自动启动

# 开机自动启动
systemctl enable redis.service
# 启动redis服务
systemctl start redis.service
# 查看服务状态
systemctl status redis.service
# 停止服务
systemctl stop redis.service
# 取消开机自动启动(卸载服务)
systemctl disabled redis.service

开放redis的应用端口

 # 开放redis端口
 firewall-cmd --zone=public --add-port=6379/tcp --permanent
 # 应用
 firewall-cmd --reload

Redis基础

Redis中的基本数据类型和常用命令

命令参考地址:redisdoc.com/string/set.…

使用Jedis连接Redis

准备以下的pom依赖

<dependencies>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
        <version>3.7.0</version>
    </dependency>

    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
</dependencies>

连接代码

public class ClientDemo {
    public static void main(String[] args) {
        //建立链接
        Jedis jedis = new Jedis("192.168.216.140",6379);
        //设置密码
        jedis.auth("123456");
        //选择数据库
        jedis.select(0);
        String isSetSuccess = jedis.set("key", "老牛爱redis01");
        String value = jedis.get("key");
        System.out.println(isSetSuccess + ":" + value);
    }
}

由于Jedis本省是线程不安全的,并且频繁的创建连接会有性能上的损耗,因此我们可以使用jedis的连接池代替原来的连接方式。

//jedis 连接池的代码
public class JedisConnectionFactory {
    private static final JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        //最大的连接数
        jedisPoolConfig.setMaxTotal(8);
        //最大的空闲连接
        jedisPoolConfig.setMaxIdle(8);
        //设置等待的时长
        jedisPoolConfig.setMinIdle(0);
        //设置连接池的等待时长
        jedisPoolConfig.setMaxWaitMillis(200);

        jedisPool = new JedisPool(jedisPoolConfig, "192.168.216.140", 6379,
                1000, "123456");
    }
    //返回jedis 对象
    public static Jedis getJedis(){
        return jedisPool.getResource();
    }
}

SpringDataRedis

SpringDataRedis 是Spring中的数据操作模块,包含了对各种数据库的集成,其中对Redis集成的模块就是SpringDataRedis

SpringDataRedis 提供了对不同Redis客户端的整合(JedisLettuce)

提供了RedisTemplate统一API来操作Redis

支持Redis 的发布订阅模式

支持Redis的哨兵和集群

支持Lettuce的响应式编程

支持基于JDK、JSON、字符串、Spring对象的数据序列化及反序列化

支持基于Redis的JDKCollection实现

API

image.png

演示案例:

1、创建一个新的模块SpringDataRedis,并且引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
    <version>2.6.2</version>
</dependency>

2、写配置文件

spring:
  redis:
    host: 192.168.216.140
    port: 6379
    password: 123456
    lettuce:
      pool:
        max-active: 8 # 最大连接
        max-idle: 8   # 最大空闲连接
        min-idle: 0   # 最小空闲连接
        max-wait: 100 # 连接等待时间

3、写测试代码

@SpringBootTest
class SpringDataRedisApplicationTests {

    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void contextLoads() {
        //向redis数据库中写入一条数据
        redisTemplate.opsForValue().set("name","niuxiaon");
        //从数据库中读取一条数据
        Object name = redisTemplate.opsForValue().get("name");
        System.out.println(name);
    }
}

执行结果:

image.png

image.png

可以看到数据库里已经添加进入了,但是会发现出现了一堆不认识的字符,这就要说一说RedisTemplate序列化的方式了,他是将Object对象序列化成字节的方式,采用的是JDK默认的序列化方式。这种序列化方式缺点就是可读性差,然后占用内存也会比较多一点。

我们可以自定义RedisTemplate的序列化方式。

编写自己指定的配置类

@Configuration
public class RedisTemplateConfig {
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        //创建Template
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        //设置工厂的连接
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer =
                new GenericJackson2JsonRedisSerializer();
        //Key和HashKey采用String的方式进行序列化
        redisTemplate.setKeySerializer(RedisSerializer.string());
        redisTemplate.setHashKeySerializer(RedisSerializer.string());

        redisTemplate.setValueSerializer(jsonRedisSerializer);
        redisTemplate.setHashValueSerializer(jsonRedisSerializer);
        return redisTemplate;
    }
}

还需要以下依赖,依赖的版本让他自己去和RedisTemplate相互适配。所以不要指定版本,否则可能会产生一些问题。

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

重新编写一个测试方法

    @Test
    public void saveUser(){
        User user = new User();
        user.setName("牛小牛");
        user.setAge(22);
        redisTemplate.opsForValue().set("user",user);
        //从数据库中读取一条数据
        Object name = redisTemplate.opsForValue().get("user");
        System.out.println(name);
    }
}

image.png

他会将这个对象的类型名称也写入进入造成了额外的内存开销。

为了解决这个问题,SpringData还提供了一个 StringRedisTemplate

image.png

image.png

Redis应用实战

实战项目使用的是黑马程序员的黑马点评项目

短信登录功能

通过Session 登录

session登录流程

image.png

项目启动以后张这个样子,部署过程略。

image.png

1、首先要完成的是验证码的获取

完善这个方法 image.png

@Override
public Result sendCode(String phone, HttpSession session) {
    //1、手机号格式校验
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式不正确");
    }
    //2、如果手机号符合要求则生成验证码
    String code = RandomUtil.randomNumbers(6);
    //3、将验证码保存到Session中
    session.setAttribute("code",code);

    //4、模拟发送验证码
    log.debug("发送短信验证码成功,验证码为{}",code);
    return Result.ok();
}

测验:输入手机号,点击发送

image.png

在这里可以看到已经成功发送了验证码

image.png

2、完成模拟登陆注册功能

image.png

完善这个login方法

@Override
public Result login(LoginFormDTO loginForm, HttpSession session) {
    String phone = loginForm.getPhone();
    //1、手机号校验
    if (RegexUtils.isPhoneInvalid(phone)) {
        return Result.fail("手机号格式不正确");
    }
    
    //2、验证码校验
    Object code = session.getAttribute("code");
    if (!code.toString().equals(loginForm.getCode()) || code == null){
        return Result.fail("验证码不正确");
    }
    //3、从数据库查询用户
    User user = query().eq("phone", phone).one();
    
    //3.1、如果这个对象为空,则创建
    if (user == null){
        user =  createUserWithPhone(phone);
    }
    //4、将用户信息保存在session中
    session.setAttribute("user",user);
    return Result.ok();
}

private User createUserWithPhone(String phone) {
    User user = new User();
    user.setPhone(phone);
    user.setNickName("user_"+RandomUtil.randomString(10));
    return user;
}

3、做我们的登录状态校验

配置拦截器

public class LoginIntercept implements HandlerInterceptor {

    //前置
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //1、获取session
        HttpSession session = request.getSession();

        //2、获取session中的内容
        Object user = session.getAttribute("user");

        //3、判断用户是否存在
        if (user == null) {
            //3.1、进行拦截
            response.setStatus(401);
            return false;
        }
        //4、将用户保存在ThreadLocal中
        UserHolder.saveUser((User) user);
        return true;
    }
    //后置
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

让拦截器生效

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercept()).excludePathPatterns(
          "/user/code",
          "/user/login",
          "/blog/hot",
          "/shop/**",
          "/shop-type/**",
          "/upload/**",
           "/voucher/**"
        );
    }
}

去通过 me方法获取到线程池里面的内容

image.png

运行测试

image.png

可以看到已经运行成功了。但是一些敏感信息也在前端返回了,这个不合理的,所以修改。这个返回内容

创建一个新的ThreadLocal

image.png

修改Login方法

image.png

修改前置拦截器

image.png

修改me方法

image.png

这个时候再次进行测试,这个时候就没有敏感信息了

image.png

通过Redis 登录

为什么要通过Redis实现登录,Session的登录方式有什么弊端。

Session 共享问题:多个tomcat 的服务器,没有办法共享Session的存储空间,当请求切换到不同的tomcat服务器时候导致数据丢失问题。

image.png

基于Redis的登录流程

image.png

使用redis以后他们的key和value分别采用什么结构比较合适?value 存储的是验证码,显然我们使用String结构即可,但是Key呢?,要保证key是唯一,如果Key不是唯一的,会导致数据覆盖问题。

用户对象使用什么结构来保存,用户对象可以使用两种结构来保存,String结构和Hash结构

image.png

1、修改发送验证码的业务逻辑

    @Autowired
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public Result sendCode(String phone, HttpSession session) {

        //Redis的方式进行验证码的发送
        //1、验证手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式不正确");
        }
        //2、如果手机号符合要求则生成验证码
        String code = RandomUtil.randomNumbers(6);
        //3、将验证码保存到Redis中
        stringRedisTemplate.opsForValue().set(RedisConstants.LOGIN_CODE_KEY +phone,code,RedisConstants.LOGIN_CODE_TTL,TimeUnit.MINUTES);
        //4、模拟发送验证码
        log.debug("发送短信验证码成功,验证码为{}",code);
        //5、返回
        return Result.ok();
    }

运行测试一下,可以看到验证码已经存到Redis数据库里面了

image.png

2、修改登录的业务逻辑

    @Override
    public 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(RedisConstants.LOGIN_CODE_KEY + phone);
        if (!cacheCode.equals(loginForm.getCode()) || cacheCode == null){
            return Result.fail("验证码不正确");
        }

        //3、从数据库查询用户
        User user = query().eq("phone", phone).one();
        //3.1、如果数据库中不存在这个用户则创建
        if (user == null){
            user =  createUserWithPhone(phone);
        }

        //4、将用户信息保存在Redis中
        //4.1、生成随机的Token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        //4.2、将User对象转为Hash进行存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO);
        //4.3、存储在redis中
        String tokenKey = RedisConstants.LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenKey,userMap);
        //设置Token有效期
        stringRedisTemplate.expire(tokenKey,30,TimeUnit.MINUTES);
        //5、返回
        return Result.ok(token);
    }

    private User createUserWithPhone(String phone) {
        User user = new User();
        user.setPhone(phone);
        user.setNickName("user_"+RandomUtil.randomString(10));
        return user;
    }

3、修改拦截器,在拦截器中做token更新操作

public class LoginIntercept implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    public LoginIntercept(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    //前置
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //1、获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            response.setStatus(401);
            return false;
        }
        //2、从Redis中获取用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
        //3、判断用户是否存在
        if (userMap.isEmpty()){
            response.setStatus(401);
            return false;
        }
        //4、将查询到的Hash数据转化成UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5、将用户的信息保存在ThreadLocal中
        UserDTOHolder.saveUser(userDTO);
        //6、刷新token的有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,
                30, TimeUnit.MINUTES);

        //7、返回
        return true;
    }
    //后置
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

4、me方法不用修改,直接操作即可

image.png

5、运行测试 image.png

可以看到用户的信息都存在了Redis中
image.png

使用redis代替session需要考虑到的问题

选择合适的数据结构,选择合适的key,选择合适的存储粒度

拦截器优化

使用第一个拦截器用来做token的动态刷新操作,第二个拦截器则是做用户检查操作

image.png

创建一个新的拦截器RefreshTokenIntercept

这个拦截器做的工作就是刷新token然后将用户保存在ThreadLocal中

public class RefreshTokenIntercept implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;
    public RefreshTokenIntercept(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }
    //前置
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //1、获取请求头中的token
        String token = request.getHeader("authorization");
        if (StrUtil.isBlank(token)){
            return true;
        }
        //2、从Redis中获取用户
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(RedisConstants.LOGIN_USER_KEY+token);
        //3、判断用户是否存在
        if (userMap.isEmpty()){
            return true;
        }
        //4、将查询到的Hash数据转化成UserDTO对象
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        //5、将用户的信息保存在ThreadLocal中
        UserDTOHolder.saveUser(userDTO);
        //6、刷新token的有效期
        stringRedisTemplate.expire(RedisConstants.LOGIN_USER_KEY+token,
                30, TimeUnit.MINUTES);

        //7、返回
        return true;
    }
    //后置
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

修改登录拦截器LoginIntercept

public class LoginIntercept implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;
    public LoginIntercept(StringRedisTemplate stringRedisTemplate) {
        this.stringRedisTemplate = stringRedisTemplate;
    }

    public LoginIntercept() {

    }

    //前置
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        //1、判断ThreadLocal中有没有我们的用户
        UserDTO user = UserDTOHolder.getUser();

        //没有用户则进行拦截
        if (user == null){
            response.setStatus(401);
            return false;
        }
        //有用户放行
        return true;
    }
    //后置
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        UserHolder.removeUser();
    }
}

注册拦截器

Order用来指定拦截器的执行顺序,值越大执行级别越低

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginIntercept()).excludePathPatterns(
          "/user/code",
          "/user/login",
          "/blog/hot",
          "/shop/**",
          "/shop-type/**",
          "/upload/**",
           "/voucher/**"
        ).order(1);

        //拦截所有的请求
        registry.addInterceptor(new RefreshTokenIntercept(stringRedisTemplate))
                .addPathPatterns("/**").order(0);
    }
}

商户查询缓存

什么是缓存?缓存就是数据交换的缓冲区,是存储数据的临时地方,一般情况下读写性能比较高。

缓存的作用:在查询的时候可以先查询缓存,降低后端的负载,提高读写效率,降低响应时间

缓存的成本:数据一致性问题,代码维护复杂度提高,运维成本。

添加Redis缓存

image.png

1、根据这个流程来添加Redis的缓存,让他先查询缓存如果缓存中没有在查询数据库。

image.png

这个方法的业务实现

@Autowired
private StringRedisTemplate stringRedisTemplate;

@Override
public Result queryById(Long id) {
    //1、从redis中查询店铺缓存
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2、判断店铺是否存在
    if (StrUtil.isNotBlank(shopJson)){
        //如果存在,则直接返回
        Shop shop = JSONUtil.toBean(shopJson, Shop.class);
        return Result.ok(shop);
    }

    //3、如果不存在,则根据id查询数据库
    Shop shop = getById(id);
    //3.1、如果查询数据库了还不存在,则返回错误信息
    if (shop == null){
        return Result.fail("店铺不存在");
    }
    //4、如果店铺存在则放入到redis中
    stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
    //5、返回
    return Result.ok(shop);
}

启动测试

可以看到这个缓存已经成功添加到Redis中了

image.png

缓存的更新策略

缓存的更新策略正是为了解决缓存和数据库数据内容不一致问题。

image.png

image.png

image.png

image.png

案例:

1、在刚才的店铺查询方法中设置他的有效期

image.png

2、完成修改店铺的操作

先修改数据库,然后在删除缓存。

image.png

完善这个更新方法

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

接下来就可以进行测试了。在这里使用postMan进行模拟测试

image.png

可以看到这个数据已经被添加到redis中去了,同时设置了他的过期时间 image.png

缓存穿透

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

解决缓存穿透的两种方案:

方案一:缓存空对象

image.png

优点:实现简单,方便维护
缺点:使用了额外的内存消耗,可能会造成短期的不一致问题

方案二:布隆过滤器

image.png

优点:内存的占用比较少,没有多余的key
缺点:可能会造成误判

案例演示:

使用方案一进行解决

image.png

1、修改店铺的查询方法

image.png

启动测试:

image.png

可以看到这个redis里面存储了一个空的字符串 image.png

总结:以及解决方案

image.png

缓存雪崩

缓存雪崩: 指的是在同一时间端大量的缓存key同时失效或者redis宕机,导致大量的请求到达数据库,带来的巨大压力

解决方案:

  • 方案1、给不同的key添加随机的时间值

  • 方案2、利用redis集群提高服务的可用性

  • 方案3、给缓存业务添加降级限流策略

  • 方案4、给业务添加多级缓存

缓存击穿

缓存击穿问题: 缓存击穿问题也称之为热点key的问题,是一个被高并发并且缓存重建业务比较复杂的key突然失效了,无数的请求访问会在一瞬去访问数据库。

常见的两种解决方案:互斥锁、逻辑过期

image.png

image.png

这两种方案的优劣对比

image.png

案例:通过互斥锁的方式用来解决缓存击穿问题

业务流程做出以下的调整

image.png

实现

    //缓存击穿
    public Shop queryWithMutex(Long id) {
        //1、从redis中查询店铺缓存
        String key = RedisConstants.CACHE_SHOP_KEY + id;
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //2、判断店铺是否存在
        if (StrUtil.isNotBlank(shopJson)){
            //如果存在,则直接返回
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return shop;
        }
        if (shopJson!=null){
            return null;
        }
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            //3、实现缓存的重建
            //3.1、获取互斥锁
            boolean isLock = tryLock(lockKey);
            //3.2、判断是否获取锁成功
            if (!isLock){
                //3.3、获取锁失败,失败以后休眠并重试
                Thread.sleep(50);
                return queryWithMutex(id);
            }
            //3.4、获取锁成功
            shop = getById(id);
            Thread.sleep(200);
            if (shop == null){
                //将空信息写入到Redis
                stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
                return null;
            }
            //4、如果店铺存在则放入到redis中
            stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop),30, TimeUnit.MINUTES);
        }catch (InterruptedException e){
            throw new RuntimeException();
        }finally {
            //5、释放互斥锁
            unlock(lockKey);
        }
        //6、返回
        return shop;
}

进行测试:

使用压力测试工具

image.png

可以看到整个过程只请求了一次数据库

image.png

案例:使用逻辑过期方式解决缓存击穿问题

逻辑删除过期方式的业务流程

image.png

1、首先要设置数据的过期时间

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

测试方法:

@SpringBootTest
class HmDianPingApplicationTests {

    @Autowired
    private ShopServiceImpl shopService;

    @Test
    public void testSaveShop(){
        shopService.saveShop2Redis(1L,10L);
    }

}

测试结果

在这里可以看到我们指定的过期时间

image.png

2、逻辑过期解决方法代码

    @Override
    public Result queryById(Long id) {
        //利用逻辑过期的方式解决缓存击穿问题
        Shop shop = queryWithLogicalExpire(id);
        return Result.ok(shop);
    }
//缓存击穿 逻辑过期方式解决
public Shop queryWithLogicalExpire(Long id){
    //1、从redis中查询店铺缓存
    String key = RedisConstants.CACHE_SHOP_KEY + id;
    String shopJson = stringRedisTemplate.opsForValue().get(key);
    //2、判断店铺是否存在
    if (StrUtil.isBlank(shopJson)){
        //如果不存在,则直接返回
        return null;
    }

    //3、如果命中了需要判断过期的时间
    RedisData redisData = JSONUtil.toBean(shopJson, RedisData.class);
    Shop shop = JSONUtil.toBean((JSONObject) redisData.getData(), Shop.class);
    LocalDateTime expireTime = redisData.getExpireTime();
    //4、判断是否过期
    if (expireTime.isAfter(LocalDateTime.now())){
        //4.1、没过期,直接返回店铺信息
        return shop;
    }
    //4.2、过期,需要重新创建缓存

    //5、缓存重新建立
    //5.1、拿到互斥锁
    String lockKey = RedisConstants.LOCK_SHOP_KEY + id;
    boolean isLock = tryLock(lockKey);
    //5.2、没有拿到互斥锁
    if (isLock){
        CACHE_REBUILD_EXECUTOR.submit(() -> {
            try {
                //重新建立缓存
                this.saveShop2Redis(id,20L);
            }catch (Exception e){
                throw new RuntimeException(e);
            }finally {
                //释放锁
                unlock(lockKey);
            }
        });
    }
    return shop;
}

运行测试

image.png

image.png

缓存工具封装

一个工具类

@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;
    }

    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);
    }
}

优惠卷秒杀

全局唯一性ID

@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.生成时间戳
        LocalDateTime now = LocalDateTime.now();
        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.拼接并返回
        return timestamp << COUNT_BITS | count;
    }
}

全唯一性ID的生成策略

  • UUID
  • Redis自增
  • 雪花算法
  • 数据库自增

Redis自增ID策略

  • 每天一个Key,方便统计订单量
  • ID构造器是时间戳 + 计数器

秒杀功能实现

image.png

@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Autowired
    private ISeckillVoucherService seckillVoucherService;
    @Autowired
    private RedisIdWorker redisIdWorker;
    @Override
    @Transactional
    public 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();
        //生成唯一ID
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //存储用户ID
        Long userId = UserDTOHolder.getUser().getId();
        voucherOrder.setUserId(userId);
        //代金卷Id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
    }
}

启动测试

此时已经实现了基本的秒杀功能,但是在高并发的场景下可能会存在买超的问题,所以还要对一下的代码进行改进

image.png

解决优惠券买超的问题

出现这种问题的原因就是,多个线程操作了同一个共享变量,这个时候我们可以使用悲观锁(synchronized、Lock)的或者乐观锁(CAS)的方式进行解决。

只需要将这段代码修改成如下的即可,使用stock这个字段用来充当version这个字段的信息,也就是使用了乐观锁的方式进行解决

image.png

image.png

小总结

image.png

一人一单

一个人只可以抢购一个单优惠券,这个时候要考虑到多线程的安全问题

主调方法

public 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("抱歉,优惠券已经被抢光了");
    }

    //使用intern()方法会直接到常量池中找相对应的字符串
    Long userId = UserDTOHolder.getUser().getId();
    synchronized (userId.toString().intern()){
        IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
        return proxy.createOrder(voucherId);
    }
}

方法抽取

因为要保证数据的一致性和原子性,所以使用@Transactional注解来控制事物。

@Transactional
public Result createOrder(Long voucherId){
    //一人一单的功能
    Long userId = UserDTOHolder.getUser().getId();
        //查询订单
        Integer count = query().eq("user_id", userId).eq("voucher_id", voucherId).count();
        if (count>0){
            return Result.fail("您已经抢购过了,请不要重复请购");
        }
    //5、扣减库存
    boolean success = seckillVoucherService.update()
            .setSql("stock = stock - 1")
            .eq("voucher_id", voucherId).gt("stock",0)
            .update();

    if (!success){
        //扣减失败
        return Result.fail("库存不足");
    }
        //6、创建订单
        VoucherOrder voucherOrder = new VoucherOrder();
        //生成唯一ID
        Long orderId = redisIdWorker.nextId("order");
        voucherOrder.setId(orderId);
        //存储用户ID
        voucherOrder.setUserId(userId);
        //代金卷Id
        voucherOrder.setVoucherId(voucherId);
        save(voucherOrder);
        return Result.ok(orderId);
}

暴露代理对象

image.png

在单机模式下这样写完全没有问题,也解决了线程的安全问题,但是但是在集群模式下,还是会出现线程的安全问题。如何可以解决这样的问题,请使用分布式锁

分布式锁

什么是分布式锁:满足分布式系统或集群模式下多进程可见并且互斥的锁。

image.png

分布式锁的实现方案

image.png

基于Redis实现分布式锁

可以利用setnx的互斥性

image.png

image.png

如果我们获取到锁了,但是同时redis服务又宕机了,这个锁没有来得及释放,其他的线程就进不来,如何解决。所以需要在添加锁的时候设置超时释放。

分布式锁实现1

1、创建接口

/**
 * 分布式锁
 */
public interface ILock {

    /**
     * 尝试获取锁
     * @param timeOutSec
     * @return
     */
    boolean tryLock(long timeOutSec);


    /**
     * 释放锁
     */
    void unlock();
}

2、接口实现

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

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

    private static final String KEY_PREFIX = "lock:";

    @Override
    public boolean tryLock(long timeOutSec) {

        long threadId = Thread.currentThread().getId();
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeOutSec, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //所释放
        stringRedisTemplate.delete(KEY_PREFIX + name);
    }
}

3、一人一单代码修改

将原来使用synchronized方式修改成分布式事务方式

image.png

4、模拟测试

在Ide工具里面将services调出来

image.png

按Ctrl + D 在这里添加一个启动类

image.png

调整nginx配置

image.png

创建两个请求用来进行模拟测试

image.png

image.png

image.png

可能会出现的问题:

image.png

分布式锁实现1:改进

修改之前的分布式锁满足: 1、在获取锁时存入线程标示(可以用UUID) 2、在释放时候先获取锁中的线程标示,判断是否与当前线程标示一致

- 如果一致则释放锁
- 如果不一致则不释放锁

修改分布式锁实现类

public class SimpleRedisLock implements ILock {

    private String name;
    private StringRedisTemplate stringRedisTemplate;

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


    private static final String KEY_PREFIX = "lock:";
    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);
    }

    @Override
    public void unlock() {
        //获取线程中的标示
        String threadId = ID_PREFIX + Thread.currentThread().getId();
        //获取锁中的标示
        String id = stringRedisTemplate.opsForValue().get(KEY_PREFIX + name);
        //判断ID是否一致
        if (threadId.equals(id)){
            //所释放
            stringRedisTemplate.delete(KEY_PREFIX + name);
        }
    }
}
分布式锁实现1:改进改进

使用Lua语言脚本来解决一致性的问题

1、创建Lua脚本

-- 这里的 KEYS[1] 就是锁的key,这里的ARGV[1] 就是当前线程标示
-- 获取锁中的标示,判断是否与当前线程标示一致
if (redis.call('GET', KEYS[1]) == ARGV[1]) then
  -- 一致,则删除锁
  return redis.call('DEL', KEYS[1])
end
-- 不一致,则直接返回
return 0

2、读取Lua脚本

image.png

3、执行调用Lua脚本

image.png

image.png

Redisson 解决线程安全问题

1、引入pom文件

<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson</artifactId>
    <version>3.13.6</version>
</dependency>

2、将RedissonClient 加入到spring中管理

@Configuration
public class RedissonConfig {
    @Bean
    public RedissonClient redissionClient(){
        Config config = new Config();
        config.useSingleServer().setAddress("redis://192.168.216.140:6379")
        .setPassword("123456");
        return Redisson.create(config);
    }
}

3、修改抢票的代码

image.png

4、并发测试

image.png

Redisson可重入锁的原理

image.png

Redisson分布式锁原理

image.png

image.png

Redisson分布式主从一致问题

image.png

image.png

一个线程两次获取锁,就是锁的重入。

秒杀优化

image.png

改进秒杀业务,提高并发

image.png

① 将优惠券信息保存在Redis中

image.png

达人探店

发布探店笔记功能

image.png

UploadController 里面文件上传的地址修改成自己的

image.png

保存信息的时候,如果出现乱码的现象,请修改yml配置

image.png

查看探店详情信息

@Override
public Result queryBlogById(Long id) {

    //1、查询blog
    Blog blog = getById(id);
    if (blog == null){
        return Result.fail("博客不存在");
    }
    //2、查询blog相关用户
    User user = userService.getById(blog.getUserId());
    blog.setName(user.getNickName()).setIcon(user.getIcon());
    return Result.ok(blog);
}

实现点赞

实现思路:

首先要知道用户点赞的是那一条评论。在这里就需要两个信息,一个是用户信息,另外一个就是评论的信息。 使用redis存储用户信息和评论信息。为了避免同一个用户重复点赞,所以可以使用set结构。

获取到用户的信息,判断当前的用户是否已经点赞。没有点赞,则可以点赞,数据库点赞字段 + 1,并将点赞的信息村到redis中。如果已经点赞,则从数据库点赞字段 - 1,并从redis中剔除点赞信息。

@Override
public Result likeBlog(Long id) {
    //1、获取登录用户
    Long userId = UserHolder.getUser().getId();
    //2、判断当前登录用户是否已经点赞
    String key = RedisConstants.BLOG_LIKED_KEY + id;
    Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
    if (BooleanUtil.isFalse(isMember)) {
        //3、如果未点赞,可以点赞
        //3.1、数据库点赞数 + 1
        //3.2、保存用户到redis的set集合
        boolean isSuccess = update().setSql("liked = liked + 1").eq("id", id).update();
        if (isSuccess) {
            stringRedisTemplate.opsForSet().add(key, userId.toString());
        }
    } else {
        //4、如果是已经点赞了,取消点赞
        //4.1、数据库点赞数量 - 1
        //4.2、吧用户从Redis中移除
        boolean isSuccess = update().setSql("liked = liked - 1").eq("id", id).update();

        if (isSuccess) {
            stringRedisTemplate.opsForSet().remove(key, userId.toString());
        }
    }
    return Result.ok();
}

实现点赞排序

//实现点赞排序功能
@Override
public Result likeBlogs(Long id) {
    String key = RedisConstants.BLOG_LIKED_KEY + id;

    Set<String> top5 = stringRedisTemplate.opsForZSet().range(key, 0, 4);

    if (top5 == null || top5.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    List<Long> ids = top5.stream().map(Long::valueOf).collect(Collectors.toList());
    String idStr = StrUtil.join(",", ids);
    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());

    return Result.ok(userDTOS);
}

image.png

好友关注

关注和取关

@Resource
private StringRedisTemplate stringRedisTemplate;

@Resource
private IUserService userService;

@Override
public Result follow(Long followUserId, Boolean isFollow) {

    //1、获取登录用户
    Long userId = UserHolder.getUser().getId();

    //关注
    if (isFollow){
        Follow follow = new Follow();
        follow.setUserId(userId).setFollowUserId(followUserId);
        boolean isSuccess = save(follow);
        if (isSuccess){
            //关注的用户集合放入到redis中
            stringRedisTemplate.opsForSet().add(RedisConstants.FOLLOWS + userId , followUserId.toString());
        }
    }else{
        //取消关注
        boolean isSuccess = remove(new QueryWrapper<Follow>().eq("user_id", userId)
                .eq("follow_user_id", followUserId)
        );
        //关注的用户集合从redis中移除
        if (isSuccess) {
            stringRedisTemplate.opsForSet().remove(RedisConstants.FOLLOWS + userId , followUserId.toString());
        }
    }

    return Result.ok();
}

@Override
public Result followOrNot(Long followUserId) {
    //1、获取登录用户
    Long userId = UserHolder.getUser().getId();
    Integer count = query().eq("user_id", userId).eq("follow_user_id", followUserId).count();
    return Result.ok(count > 0);
}

共同关注

查询用户信息

图片.png

查看用户发布的博文

图片.png

共同关注的方法

@Override
public Result followCommons(Long id) {

    //1、获取当前用户
    Long userId = UserHolder.getUser().getId();


    String key1 = RedisConstants.FOLLOWS + userId;
    String key2 = RedisConstants.FOLLOWS + id;

    Set<String> intersect = stringRedisTemplate.opsForSet().intersect(key1, key2);
    if (intersect == null || intersect.isEmpty()){
        return Result.ok(Collections.emptyList());
    }
    //2、求交集
    List<Long> ids = intersect.stream().map(Long::valueOf).collect(Collectors.toList());

    //3、根据交集进行用户查询
    List<User> users = userService.listByIds(ids);
    return Result.ok(users);
}

关注推送

Feed流

图片.png

图片.png

推送到粉丝收件箱

Feed流中的数据会不断的进行更新,所以角标也会不断的进行变化,所以不能使用角标

@Override
public Result saveBlog(Blog blog) {
    // 获取登录用户
    UserDTO user = UserHolder.getUser();
    blog.setUserId(user.getId());
    // 保存探店博文
    boolean isSuccess = save(blog);
    if (!isSuccess){
        return Result.fail("新增笔记失败");
    }

    //查询笔记作者的所有粉丝
    List<Follow> follows = followService.query().eq("follow_user_id", user.getId()).list();
    //将笔记id推送给所有粉丝
    for (Follow follow : follows) {
        Long userId = follow.getUserId();
        String key = RedisConstants.FEED_KEY + userId;
        stringRedisTemplate.opsForZSet().add(key,blog.getId().toString(),System.currentTimeMillis());
    }
    // 返回id
    return Result.ok(blog.getId());
}

滚动刷新

@Override
public Result queryBlogOfFellow(Long max, Integer offset) {
    //1、获取当前用户
    Long userId = UserHolder.getUser().getId();
    //2、查询收件箱
    String key = RedisConstants.FEED_KEY + userId;
    Set<ZSetOperations.TypedTuple<String>> typedTuples = stringRedisTemplate.opsForZSet().reverseRangeByScoreWithScores(key, 0, max, offset, 2);
    //3、非空判断
    if (typedTuples == null || typedTuples.isEmpty()) {
        return Result.ok();
    }

    //4、解析数据:blogId、minTime、offset
    long minTime = 0;
    int os = 1;
    List<Long> ids = new ArrayList<>(typedTuples.size());
    for (ZSetOperations.TypedTuple<String> tuple : typedTuples) {
        String idStr = tuple.getValue();
        ids.add(Long.valueOf(idStr));
        Long time = tuple.getScore().longValue();
        if (time == minTime) {
            os++;
        }else{
            minTime = time;
            os = 1;
        }
    }
    //5、根据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) {
        //2、查询blog相关用户
        User user = userService.getById(blog.getUserId());
        blog.setName(user.getNickName()).setIcon(user.getIcon());
        //3、查询blog是否被点赞
        isBlogLiked(blog);
    }
    //6、封装并及逆行拼接
    return Result.ok(blogs);
}

附近商店

附近店铺信息导入

@GetMapping("/importData")
public Result importData(){
    //1、查询店铺信息
    List<Shop> list = shopService.list();
    //2、把店铺分组,按照typeId,typeId一致的放到一个集合
    Map<Long, List<Shop>> map = list.stream().collect(Collectors.groupingBy(Shop::getTypeId));
    //3、分配完成写入redis
    for (Map.Entry<Long, List<Shop>> entry : map.entrySet()) {
        //3.1、获取同类型的店铺id
        Long typeId = entry.getKey();
        String key = RedisConstants.SHOP_GEO_KEY + typeId;
        //3.2、获取同类型的店铺的集合
        List<Shop> value = entry.getValue();
        List<RedisGeoCommands.GeoLocation<String>> locations = new ArrayList<>();

        //3.3、写入redis geoadd key 经度 维度
        for (Shop shop : value) {
            locations.add(new RedisGeoCommands.GeoLocation<>(
                    shop.getId().toString(),
                    new Point(shop.getX(),shop.getY())
            ));
        }

        stringRedisTemplate.opsForGeo().add(key,locations);
    }

代码实现

@Override
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.getRecords());
    }
    //2、计算分页参数
    int from = (current - 1) * SystemConstants.DEFAULT_PAGE_SIZE;
    int end = current * SystemConstants.DEFAULT_PAGE_SIZE;
    //3、查询redis,按照距离排序,分页,结果shopId,distance
    String key = RedisConstants.SHOP_GEO_KEY + typeId;
    GeoResults<RedisGeoCommands.GeoLocation<String>> results = stringRedisTemplate.opsForGeo().search(
            key,
            GeoReference.fromCoordinate(x, y),
            new Distance(5000),
            RedisGeoCommands.GeoSearchCommandArgs.newGeoSearchArgs().includeCoordinates().limit(end)
    );
    //4、解析出id
    if (results == null){
        return Result.ok(Collections.emptyList());
    }
    List<GeoResult<RedisGeoCommands.GeoLocation<String>>> list = results.getContent();

    if (list.size() <= from){
        return Result.ok(Collections.emptyList());
    }


    //4.1、截取from ~ end部分
    List<Long> ids = new ArrayList<>(list.size());
    Map<String,Distance> distanceMap = new HashMap<>(list.size());
    list.stream().skip(from).forEach(result -> {
        String shopIdStr = result.getContent().getName();
        ids.add(Long.valueOf(shopIdStr));
        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();
    //6、返回
    for (Shop shop : shops) {
        shop.setDistance(distanceMap.get(shop.getId().toString()).getValue());
    }

    return Result.ok(shops);
}

用户签到

使用bitMap 使用位运算来实现

用户签到

@Override
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 = RedisConstants.USER_SIGN_KEY+userId+keySuffix;
    //4、获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
    return Result.ok();
}

统计用户连续签到

@Override
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 = RedisConstants.USER_SIGN_KEY+userId+keySuffix;
    //4、获取今天是本月的第几天
    int dayOfMonth = now.getDayOfMonth();
    //5、获取本月截至今天为止的所有签到记录,返回的是一个十进制的数字
    List<Long> result = stringRedisTemplate.opsForValue().bitField(
            key,
            BitFieldSubCommands.create()
                    .get(BitFieldSubCommands.BitFieldType.unsigned(dayOfMonth)).valueAt(0)
    );
    if (result == null || result.isEmpty()) {
        //没有任何的签到结果
        return Result.ok(0);
    }
    //6、循环遍历
    Long num = result.get(0);
    if (num == null || num == 0){
        return Result.ok(0);
    }

    int count = 0;
    while(true){
        if ((num & 1) == 0){
            //判断这个比特位是否为0
            break;
        }else{
            //不为0,证明已经签到,计数器+1
            count++;
        }
        num>>>=1;
    }
    return Result.ok(count);
}

UV统计

图片.png ---- 持续更新种 ------