Redis基础知识

1 初识Redis

Redis(Remote Dictionary Server,远程词典服务器)是一个基于内存的健值型数据库,特征如下:

1、健值型,value支持多种不同数据类型

2、单线程,每个命令具备原子性

3、低延迟、速度快(基于内存、IO多路复用、良好的编码),基于C语言实现

4、支持数据持久化

5、支持主从集群、分片集群

6、支持多语言客户端

redis-server 启动Redis

ps -ef | grep redis 查看Redis进程

redis-cli -h 127.0.0.1 -p 6379 连接redis服务

Redis中数据类型,Key一般是String类型,Value包含以下类型:

GEO:地理坐标

2 Redis命令

2.1 Redis通用命令

KEYS:查看所有符合指定模版的key。搜索效率低,生产环境不要用该命令。

查询所有的key keys *

查询所有以a开头的key keys a*

DEL:删除指定的key,可以一个或者多个key。返回值是删除了多少个key。

Exists:判断key是否存在

expire:设置key的有效期,单位是秒,有效期过后自动删除。不设置有效时间的话,默认永久有效(返回-1)

ttl:查看一个key的剩余有效期

2.2 string类型常用命令

set:添加或者修改已经存在的一个string类型的键值对

get:根据key获取string类型的value

mset:批量添加多个string类型的键值对

mget:批量获取多个key的value

Incr:让一个整型key自增1

Incrby:让一个整型的key自增并指定步长,例如incrby num 2 让num值自增2

Incrbyfloat:让一个浮点型数字自增并指定步长

setnx:添加一个string类型的键值对,前提是这个key不存在,否则不执行

setex:添加一个string类型的键值对,并指定有效期。

setex name2 10 jack等同于set name2 jack ex 10

实际项目中key要用多个单词形成层级结构,多个单词之间用:隔开,格式如下:

项目名:业务名:类型:id。如果Value是一个对象,可以将对象序列化为JSON字符串后存储

2.3 hash类型常用命令

hset key field value:添加或者修改hash类型key的field的值

hget key field:获取一个hash类型key的field的值

hmset:批量添加多个hash类型key的field的值

hmget:批量获取多个hash类型key的field的值

HgetAll:获取一个hash类型的key中的所有的field和value

Hkeys:获取一个hash类型的key中的所有的field

hvals:获取一个hash类型的key中所有的value

Hincrby:让一个hash类型key的字段值自增并指定步长

hsetnx:添加一个hash类型的key的filed值,前提是这个field不存在,否则不执行。

2.4 list类型常用命令

Redis中的List类型可以看做是一个双向链表结构,既可以支持正向检索也可以支持反向检索。List中的元素有序排列、元素可以重复,插入和删除速度快、查询速度一般。

2.5 set类型常用命令

Redis中的Set结构可以看做是一个value为null的HashMap,底层也是Hash表结构,特点如下:

1、元素无序

2、元素不可重复

3、查找速度快

4、支持交集、并集、差集等功能

单个set操作:

多个set间互相操作:

2.6 SortedSet类型常用命令

Redis中的SortedSet是一个可排序的set集合,SortedSet中每一个元素都带有

一个score属性,可以基于score属性对元素排序,底层实现是一个跳表(SkipList)加Hash表,常用来实现排行榜功能。特征如下:

1、可排序,每个元素都带score属性,根据score属性排序

2、元素不可重复

3、查询速度快

3 Redis的Java客户端

3.1 Jedis

官网地址:https://github.com/redis/jedis

引入依赖:

<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>4.3.0</version>
</dependency>

基本用法:

//Jedis中的方法名和Redis命令行命令名相同   
 Jedis jedis = new Jedis("127.0.0.1",6379);//建立连接
    @Test
    public void testJedis() {
        jedis.select(0);//选择数据库
        System.out.println( jedis.get("age"));//取数据
        jedis.setnx("school","bupt");//存数据
        if (jedis!=null) {
            jedis.close();//释放资源
        }
    }

3.1.1 Jedis连接池

public class JedisConnectionFactory {
    private static final JedisPool jedisPool;

    static {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        //最大连接数
        jedisPoolConfig.setMaxTotal(10);
        //最大空闲连接,超过一段时间后空闲连接会被撤销
        jedisPoolConfig.setMaxIdle(10);
        //最小空闲连接
        jedisPoolConfig.setMinIdle(2);
        //设置最长等待时间,毫秒
        jedisPoolConfig.setMaxWaitMillis(200);
        jedisPool = new JedisPool(jedisPoolConfig, "127.0.0.1", 6379, 1000);
    }

    /**
     * 获取jedis对象
     * @return
     */
    public static Jedis getJedis() {
        return jedisPool.getResource();
    }
}

3.2 SpringDataRedis

SpringData是Spring中数据操作的模块,包含对各种数据库的集成,其中对Redis的集成模块叫做SpringDataRedis,官网地址:https://spring.io/projects/spring-data-redis

引入依赖:

        <!-- Redis依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 连接池依赖 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-pool2</artifactId>
        </dependency>

yaml配置文件:

spring:
  data:
    redis:
      host: 127.0.0.1
      jedis:
        pool:
          max-active: 8
          max-idle: 8
          min-idle: 0
          max-wait: 100

注入RedisTemplate:

@SpringBootTest
public class RedisTest {
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    void testString() {
        redisTemplate.opsForValue().set("salary",500000);//存储数据
        System.out.println( redisTemplate.opsForValue().get("salary"));//查询数据

    }
}

3.2.1 RedisTemplate

SpringDataRedis中提供了RedisTemplate工具类,其中封装了各种对Redis的操作。并且将不同数据类型的操作API封装到了不同类型中:

4 Redis实战案例

4.1 基于redis实现短信登录

  1. 发送短信验证码

用户输入手机号后,点击“发送验证码”,请求api/user/code?phone=13456781234接口,将手机号码发送到服务端,code接口response中不需要携带具体数据。服务端生成验证码,保存验证码到session中,然后向指定的手机号发送验证码。

请求方式:post

请求路径:user/code

请求参数:phone,手机号码

返回值:无

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

}

public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);

}

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码;生成6位随机数字
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.调用验证码服务,发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result {
    private Boolean success;
    private String errorMsg;
    private Object data;
    private Long total;

    public static Result ok(){
        return new Result(true, null, null, null);
    }
    public static Result ok(Object data){
        return new Result(true, null, data, null);
    }
    public static Result ok(List<?> data, Long total){
        return new Result(true, null, data, total);
    }
    public static Result fail(String errorMsg){
        return new Result(false, errorMsg, null, null);
    }
}
  1. 短信验证码登录、注册

用户输入验证码后,点击“登录”请求/user/login接口,将手机号和验证码放到body中。

请求方式:post

请求路径:user/login

请求参数:phone和code

返回值:无

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private IUserService userService;

    @Resource
    private IUserInfoService userInfoService;

    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone, session);
    }

    /**
     * 登录功能
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码。RequestBody注解接收请求体中的phone、code、password参数
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session){
        // 实现登录功能
        return userService.login(loginForm, session);
    }
}

public interface IUserService extends IService<User> {

    Result sendCode(String phone, HttpSession session);

    Result login(LoginFormDTO loginForm, HttpSession session);

}

import lombok.Data;

@Data
public class LoginFormDTO {
    private String phone;
    private String code;
    private String password;
}

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public Result sendCode(String phone, HttpSession session) {
        // 1.校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.符合,生成验证码;生成6位随机数字
        String code = RandomUtil.randomNumbers(6);

        // 4.保存验证码到 redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY + phone, code, LOGIN_CODE_TTL, TimeUnit.MINUTES);

        // 5.调用验证码服务,发送验证码
        log.debug("发送短信验证码成功,验证码:{}", code);
        // 返回ok
        return Result.ok();
    }

    @Override
    public Result login(LoginFormDTO loginForm, HttpSession session) {
        // 1.校验手机号
        String phone = loginForm.getPhone();
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 3.从redis获取验证码并校验
        String cacheCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);
        String code = loginForm.getCode();
        if (cacheCode == null || !cacheCode.equals(code)) {
            // 不一致,报错
            return Result.fail("验证码错误");
        }

        // 4.一致,根据手机号查询用户 select * from tb_user where phone = ?   使用myBatisPlus实现查询数据库
        User user = query().eq("phone", phone).one();
        // 5.判断用户是否存在;数据库中存在该手机号的话,说明是已经注册的用户;数据库中没有该手机号的话,说明用户没有注册过。
        if (user == null) {
            // 6.不存在,创建新用户并保存
            user = createUserWithPhone(phone);
        }

        // 7.保存用户信息到 redis中
        // 7.1.随机生成token,作为登录令牌
        String token = UUID.randomUUID().toString(true);
        // 7.2.将User对象转为HashMap存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
        // 7.3.存储
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey, userMap);
        // 7.4.设置token有效期
        stringRedisTemplate.expire(tokenKey, LOGIN_USER_TTL, TimeUnit.MINUTES);

        // 8.返回token
        return Result.ok(token);
    }

     private User createUserWithPhone(String phone) {
        // 1.创建用户,保存手机号和昵称,其他字段先不管,以后用户可以更新资料
        User user = new User();
        user.setPhone(phone);
        user.setNickName(USER_NICK_NAME_PREFIX + RandomUtil.randomString(10));//昵称随机生成
        // 2.保存用户,使用myBatisPlus入库
        save(user);
        return user;
     }
}

3 使用拦截器实现登录校验

用户的所有请求先经过拦截器,在拦截器中判断用户的登录状态,然后再请求具体的服务。

//配置拦截器
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1.判断是否需要拦截(ThreadLocal中是否有用户)
        if (UserHolder.getUser() == null) {
            // 没有,需要拦截,设置状态码
            response.setStatus(401);//401表示未授权
            // 拦截
            return false;
        }
        // 有用户,则放行
        return true;
    }
}


public class RefreshTokenInterceptor implements HandlerInterceptor {

    private StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(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.基于TOKEN获取redis中的用户
        String key  = LOGIN_USER_KEY + token;
        Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries(key);
        // 3.判断用户是否存在
        if (userMap.isEmpty()) {
            return true;
        }
        // 5.将查询到的hash数据转为UserDTO
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        // 6.存在,保存用户信息到 ThreadLocal
        UserHolder.saveUser(userDTO);
        // 7.刷新token有效期
        stringRedisTemplate.expire(key, LOGIN_USER_TTL, TimeUnit.MINUTES);
        // 8.放行
        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 移除用户
        UserHolder.removeUser();
    }
}



//工具类,封装ThreadLocal
public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

//配置拦截器
@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 登录拦截器,只拦截需要登录的请求
        registry.addInterceptor(new LoginInterceptor())
                .excludePathPatterns(
                        "/shop/**",
                        "/voucher/**",
                        "/shop-type/**",
                        "/upload/**",
                        "/blog/hot",
                        "/user/code",
                        "/user/login"
                ).order(1);
        // 拦截所有请求,token刷新的拦截器,目的在于刷新token的有效期;RefreshTokenInterceptor拦截器优先级高,先执行;LoginInterceptor优先级低,后执行;拦截器优先级通过order控制
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate)).addPathPatterns("/**").order(0);
    }
}


//获取当前登录的用户并返回
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {
    @GetMapping("/me")
    public Result me(){
        // 获取当前登录的用户并返回
        UserDTO user = UserHolder.getUser();
        return Result.ok(user);
    }
}

每个Tomcat服务有自己的session存储空间,多个Tomcat服务之间session存储空间隔离,session无法共享。多个Tomcat服务可以访问Redis内存空间。

4.2 商户查询缓存

@RestController
@RequestMapping("/shop")
public class ShopController {

    @Resource
    public IShopService shopService;

    /**
     * 根据id查询商铺信息
     * @param id 商铺id
     * @return 商铺详情数据
     */
    @GetMapping("/{id}")
    public Result queryShopById(@PathVariable("id") Long id) {
        return shopService.queryById(id);
    }
}

public interface IShopService extends IService<Shop> {
    Result queryById(Long id);
}

public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {


    @Resource
    private StringRedisTemplate stringRedisTemplate;

    @Resource
    private CacheClient cacheClient;

    @Override
    public Result queryById(Long id) {
        // 解决缓存穿透
        Shop shop = cacheClient
                .queryWithPassThrough(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 互斥锁解决缓存击穿
        // Shop shop = cacheClient
        //         .queryWithMutex(CACHE_SHOP_KEY, id, Shop.class, this::getById, CACHE_SHOP_TTL, TimeUnit.MINUTES);

        // 逻辑过期解决缓存击穿
        // Shop shop = cacheClient
        //         .queryWithLogicalExpire(CACHE_SHOP_KEY, id, Shop.class, this::getById, 20L, TimeUnit.SECONDS);

        if (shop == null) {
            return Result.fail("店铺不存在!");
        }
        // 7.返回
        return Result.ok(shop);
    }
     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;
    }

}

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("tb_shop")
public class Shop implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 商铺名称
     */
    private String name;

    /**
     * 商铺类型的id
     */
    private Long typeId;

    /**
     * 商铺图片,多个图片以','隔开
     */
    private String images;

    /**
     * 商圈,例如陆家嘴
     */
    private String area;

    /**
     * 地址
     */
    private String address;

    /**
     * 经度
     */
    private Double x;

    /**
     * 维度
     */
    private Double y;

    /**
     * 均价,取整数
     */
    private Long avgPrice;

    /**
     * 销量
     */
    private Integer sold;

    /**
     * 评论数量
     */
    private Integer comments;

    /**
     * 评分,1~5分,乘10保存,避免小数
     */
    private Integer score;

    /**
     * 营业时间,例如 10:00-22:00
     */
    private String openHours;

    /**
     * 创建时间
     */
    private LocalDateTime createTime;

    /**
     * 更新时间
     */
    private LocalDateTime updateTime;


    @TableField(exist = false)
    private Double distance;
}

4.3 博客

4.4 优惠券秒杀

4.5 好友关注

4.6 附近的商户

4.7 用户签到

4.8 UV统计

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值