Redis

基础篇

 

127.0.0.1:6379> LPUSH user 1 2 3 4 5 6

 

127.0.0.1:6379> smembers si
1) "b"
2) "d"
3) "c"
4) "a"
127.0.0.1:6379> sadd s1 a e l c
(integer) 4
127.0.0.1:6379> sinter si s1
1) "c"
2) "a"

可以用于好友列表找共同好友

 

排行榜作用

 

Jedis连接池 

 避免了jedis中类对象作为值要先转为json的麻烦

set的name和不同的不一样,存进去的被当作了对象做序列化

掌握此方法

package com.example;

import com.example.pojo.User;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.SneakyThrows;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.StringRedisTemplate;

@SpringBootTest
class redisStringTests {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    @Test
    void testString() {//存字符串
        stringRedisTemplate.opsForValue().set("new_name","虎哥");
        Object name=stringRedisTemplate.opsForValue().get("new_name");
        System.out.println(name);
    }
    private static final ObjectMapper mapper=new ObjectMapper();//将对象转为json的工具
    @SneakyThrows
    @Test
    void testUser() {//存字符串
        User uu=new User("克洛克达尔",25);
        //手动序列化
        String json=mapper.writeValueAsString(uu);
        stringRedisTemplate.opsForValue().set("海贼",json);
        String jsonUser=stringRedisTemplate.opsForValue().get("海贼");
        //手动反序列化
        User u1=mapper.readValue(jsonUser,User.class);
        System.out.println(u1);
    }

}

 

对于存hash ,一个key有多个字段

实战篇 

短信登录

 

 

 session的缺点

用redis替代session 

redis是共享的

 

        //保存用户信息到redis
        //7.1 随机生成token,作为登录令牌
        String token=UUID.randomUUID().toString();
        //将User转为HashMap存储
        UserDTO userDTO=BeanUtil.copyProperties(user,UserDTO.class);
        Map<String,Object> usermap=BeanUtil.beanToMap(userDTO);//此时user中id是long类型
        String tokenKey = LOGIN_USER_KEY + token;
        stringRedisTemplate.opsForHash().putAll(tokenKey+token,usermap);
        //设置日期,用户在30分钟内进行了操作就不会清除
        stringRedisTemplate.expire(tokenKey,LOGIN_USER_TTL,TimeUnit.MINUTES);
        return Result.ok();

 拦截器优化

不需要做登录校验的页面,也需要刷新

 商户查询缓存

 

 

 缓存更新策略

 线程不安全

 企业使用缓存的难点问题

布隆过滤器是数据库的哈希,但不准确 

        String key=CACHE_SHOP_KEY + id;
        //先从Redis中查,这里的常量值是固定的前缀 + 店铺id
        String shopJson = stringRedisTemplate.opsForValue().get(key);
        //如果不为空(查询到了),则转为Shop类型直接返回
        if (StrUtil.isNotBlank(shopJson)) {
            Shop shop = JSONUtil.toBean(shopJson, Shop.class);
            return Result.ok(shop);
        }
        if(shopJson!=null){
            return Result.fail("店铺不存在!!");
        }
        //否则去数据库中查
        Shop shop = getById(id);
        //查不到返回一个错误信息或者返回空都可以,根据自己的需求来
        if (shop == null){
            stringRedisTemplate.opsForValue().set(key,"",CACHE_NULL_TTL,TimeUnit.MINUTES);
            return Result.fail("店铺不存在!!");
        }
        //查到了则转为json字符串
        String jsonStr = JSONUtil.toJsonStr(shop);
        //并存入redis
        stringRedisTemplate.opsForValue().set(key, jsonStr,CACHE_SHOP_TTL, TimeUnit.MINUTES);
        //最终把查询到的商户信息返回给前端
        return Result.ok(shop);

 

 

 

互斥锁是自定义的

原理是:set一个键值对时,如果已经存在,就不会设置成功,否则就是成功,在这个基础上添加键值对生存时间,这样相当于就创立了一个锁,其他线程进行时,设置返回为false,代表没抢到锁

 

优惠券秒杀 

全局唯一ID

 

package com.hmdp.utils;

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

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

public class RedisIdWorker {
    private StringRedisTemplate stringRedisTemplate;
    //设置起始时间,我这里设定的是2022.01.01 00:00:00
    public static final Long BEGIN_TIMESTAMP = 1640995200L;
    //序列号长度
    public static final Long COUNT_BIT = 32L;

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


    public long nextId(String keyPrefix) {
        //1. 生成时间戳
        LocalDateTime now = LocalDateTime.now();
        long currentSecond = now.toEpochSecond(ZoneOffset.UTC);
        long timeStamp = currentSecond - BEGIN_TIMESTAMP;
        //2. 生成序列号
        String date = now.format(DateTimeFormatter.ofPattern("yyyy:MM:dd"));
        long count = stringRedisTemplate.opsForValue().increment("inc:" + keyPrefix + ":" + date);
        //3. 拼接并返回,简单位运算
        return timeStamp << COUNT_BIT | count;

    }
}

秒杀

秒杀券其实是普通券的附加信息, 普通券中被标注了的,就会在秒杀券表中寻找其信息

package com.hmdp.service.impl;

import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
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 org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class VoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {
    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    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();
        // 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);

    }
}

因为要操作两个库,所以设置为事务,进行滚动

高并发的超卖问题

 

可以将库存本身当作版本号

 

 但是弊端就是抢失败人数提升,100个人,只要有一个人修改了,其他人全部失败

改进:

stock>0

//5,扣减库存
        boolean success = seckillVoucherService.update()
                .setSql("stock = stock - 1")
                .eq("voucher_id", voucherId)
                .gt("stock", 0)
                .update();
        if (!success) {
            //扣减库存
            return Result.fail("库存不足!");
        }

 

一人一单 

也是有并发的风险

 加悲观锁

事物提交之后加锁

且锁只争对相同用户,用户ID为锁对象

将悲观锁加到抽象出的函数中,事物提交了才释放锁,但是此时该函数被调用是没有事务性的,需要拿到代理对象来调用该函数

只适合在单体,不适合集群

分布式

不同JVM锁监视器对象不同 

 

 

package com.hmdp.utils;

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

public class SimpleRedisLock implements ILock{
    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) {
        //获取线程标识
        long threadId = Thread.currentThread().getId();
        //获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId + "", timeoutSec, TimeUnit.SECONDS);
        //自动拆箱可能会出现null,这样写更稳妥
        return Boolean.TRUE.equals(success);
    }

    @Override
    public void unlock() {
        //通过DEL来删除锁
        stringRedisTemplate.delete(KEY_PREFIX + name);

    }
}

    Long userId = UserHolder.getUser().getId();
        // 创建锁对象
        SimpleRedisLock redisLock = new SimpleRedisLock("order:" + userId, stringRedisTemplate);
        // 获取锁对象
        boolean isLock = redisLock.tryLock(120);
        // 加锁失败,说明当前用户开了多个线程抢优惠券,但是由于key是SETNX的,所以不能创建key,得等key的TTL到期或释放锁(删除key)
        if (!isLock) {
            return Result.fail("不允许抢多张优惠券");
        }
        try {
            // 获取代理对象
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.createVoucherOrder(voucherId);
        } finally {
            // 释放锁
            redisLock.unlock();
        }

 存在的问题

释放了别人的锁

解决办法,释放锁的适合看看线程的标识是否和自己的一样

在集群模式下,线程ID是有相同的,所以使用UUID +ID

    @Override
    public boolean tryLock(long timeoutSec) {
        //获取线程标识
        String threadId = ID_PREFIX+Thread.currentThread().getId();
        //获取锁,使用SETNX方法进行加锁,同时设置过期时间,防止死锁
        Boolean success = stringRedisTemplate.opsForValue().setIfAbsent(KEY_PREFIX + name, threadId , timeoutSec, TimeUnit.SECONDS);
        //自动拆箱可能会出现null,这样写更稳妥
        return Boolean.TRUE.equals(success);
    }

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

 还存在问题,判断锁标识和释放锁是两个动作,必须要保证原子性

 

分布式优化 

 

阻塞式拿锁 

按照之前的方法,是不能实现重复获取锁的 

 

哈希结构来存储,记录重复拿的次数,当到了最外层,value=0,就删除锁

为了保证原子性,用lua脚本

 

解决办法:要在每一个主节点上拿到锁才算真正获取 

创建连锁

@Resource
private RedissonClient redissonClient;
@Resource
private RedissonClient redissonClient2;
@Resource
private RedissonClient redissonClient3;

private RLock lock;

@BeforeEach
void setUp() {
    RLock lock1 = redissonClient.getLock("lock");
    RLock lock2 = redissonClient2.getLock("lock");
    RLock lock3 = redissonClient3.getLock("lock");
    lock = redissonClient.getMultiLock(lock1, lock2, lock3);
}

 

异步秒杀

 

开辟独立线程 执行数据库mysql的读写,用阻塞队列装订单id等,写mysql,就不用那么急,不需要高时效性

添加阻塞队列(当里面有元素才能被执行取出操作)后,利用线程池,每个线程独立将信息存入数据库 

消息队列实现异步秒杀 

 消息队列比阻塞队列的优点:1、不受jvm内存限制2、消息持久化,安全

 

steam做消息队列 

 

 

 

 

 

达人探店 

    @TableField(exist = false)
    private String icon;
    /**
     * 用户姓名
     */
    @TableField(exist = false)
    private String name;
    /**
     * 是否点赞过了
     */
    @TableField(exist = false)
    private Boolean isLike;

注解表示这些字段不属于该表,后期手动维护就行

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.hmdp.dto.Result;
import com.hmdp.entity.Blog;
import com.hmdp.entity.User;
import com.hmdp.mapper.BlogMapper;
import com.hmdp.service.IBlogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.service.IUserService;
import com.hmdp.utils.SystemConstants;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.List;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class BlogServiceImpl extends ServiceImpl<BlogMapper, Blog> implements IBlogService {
    @Resource
    private IUserService userService;
    @Override
    public Result queryHotById(Integer current) {
        // 根据用户查询
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(this::queryBlogUser);
        return Result.ok(records);
    }

    @Override
    public Result queryBlogById(Long id) {
        Blog blog = getById(id);
        if (blog == null) {
            return Result.fail("评价不存在或已被删除");
        }
        queryBlogUser(blog);
        return Result.ok(blog);
    }
    private void queryBlogUser(Blog blog) {
        Long userId = blog.getUserId();
        User user = userService.getById(userId);
        blog.setName(user.getNickName());
        blog.setIcon(user.getIcon());
    }
}

点赞操作

     * 是否点赞过了
     */
    @TableField(exist = false)
    private Boolean isLike;

点赞排行

共同好友

附近商铺

用户签到

 

节省内存 ,将每个用户的当前月作为key

 签到统计

UV统计 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值