Redis学习笔记

这里写目录标题

解决集群的session共享问题

使用rediss代替session共享登陆问题



每次请求通过携带token到redis中判断是否存在该用户。

修改保存验证码的方式为redis 【String类型】

    @Override
    public Result sedCode(String phone, HttpSession session) {
        //1. 校验手机号
        if (RegexUtils.isPhoneInvalid(phone)) {
            //2.如果不符合,返回错误信息
            return Result.fail("手机号格式错误");
        }
        //3. 符合,生成验证码
        String code = RandomUtil.randomNumbers(6);
        //4. 保存验证码到redis 添加业务前缀来区分。
        //并且添加redis的key的有效期
     stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code,LOGIN_CODE_TTL ,TimeUnit.MINUTES);
        //5. 发送验证码
        log.debug("发送短信验证码成功,验证码:{}",code);
        //返回ok
        return Result.ok();
    }

登录的时候发送token到redis【Map类型】

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

        //4.一致,根据手机号查询用户
        User user = query().eq("phone", phone).one();

        //5. 判断用户是否存在
        if (user == null){
            //6. 不存在,创建新用户
            user = createUserWithPhone(phone);
        }
        //7.保存用户信息到session'
        //7,1随机生产token作为登陆令牌
         String token = UUID.randomUUID().toString().replace("-", "");
        //7.2 将user对象转为hash存储
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> usermap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
        .setIgnoreNullValue(true)
        .setFieldValueEditor((fileName,fieldValue) -> fieldValue.toString()));
        //7.3 使用has结构存储
        String tokenkey =LOGIN_USER_KEY+token;
        stringRedisTemplate.opsForHash().putAll(tokenkey,usermap);
        //7.4给token设置有效期
        stringRedisTemplate.expire(tokenkey,CACHE_SHOP_TTL,TimeUnit.MINUTES);
        return Result.ok();
    }

把类型列表写入缓存中【List类型】

 @GetMapping("list")
    public Result queryTypeList() {
        //1.从redis查询分类缓存
        List<String> shopTypeList = new ArrayList<>();
        shopTypeList = stringRedisTemplate.opsForList().range(SHOP_TYPE,0,-1);
        if (!shopTypeList.isEmpty()) {
            List<ShopType> typeList = new ArrayList<>();
            for (String str:shopTypeList) {
                ShopType shopType = JSONUtil.toBean(str, ShopType.class);
                typeList.add(shopType);
            }
            return Result.ok(typeList);

        }
        //2.不存在就查数据库
        List<ShopType> typeList = typeService
                .query().orderByAsc("sort").list();

        if (typeList==null && typeList.size()>0){
            return Result.fail("没有商品类型信息");
        }
        //存在添加redis缓存
        for(ShopType shopType : typeList){
            String s = JSONUtil.toJsonStr(shopType);
            shopTypeList.add(s);
        }
        stringRedisTemplate.opsForList().rightPushAll(SHOP_TYPE,shopTypeList);
        //3.转成json格式写入redis
        return Result.ok(typeList);
    }

使用拦截器刷新用户有效期

先刷新用户的token如果不存在就接着往下走,然后走到下一个拦截器中拦截。

package com.hmdp.utils;

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.LOGIN_USER_KEY;
import static com.hmdp.utils.RedisConstants.LOGIN_USER_TTL;

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

下一个拦截器来判断是否登陆了账号

package com.hmdp.utils;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.StrUtil;
import com.hmdp.dto.UserDTO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.Map;
import java.util.concurrent.TimeUnit;

public class LoginInterceptor implements HandlerInterceptor {
    private StringRedisTemplate stringRedisTemplate;


    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
       //1.是否需要去拦截
        if (UserHolder.getUser()==null){
            // 没有拦截设置状态吗
            response.setStatus(401);
            return false;
        }
        return true;
    }


}

redis的缓存



添加缓存店铺查看缓存

package com.hmdp.service.impl;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import com.hmdp.service.IShopService;
import com.hmdp.utils.CacheClient;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

import static com.hmdp.utils.RedisConstants.CACHE_SHOP_KEY;
import static com.hmdp.utils.RedisConstants.CACHE_SHOP_TTL;

/**
 * <p>
 * 服务实现类
 * </p>
 *
 * @author 虎哥
 * @since 2021-12-22
 */
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
    @Resource
    private StringRedisTemplate stringRedisTemplate;

    //编写查询和缓存流程
    @Override
    public Result querybyId(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //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("店铺吧存在");
        }
        //6.不存在,写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop));
        return Result.ok(shop);
    }
}

缓存更新策略




先操作数据库在写入缓存出现异常的情况较少。

先修改数据库在更新缓存

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

缓存穿透


解决方案 添加空值返回

   @Override
    public Result querybyId(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //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) {
               stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,"",CACHE_NULL_TTL, TimeUnit.MINUTES);
            return Result.fail("店铺吧存在");
        }
        //6.不存在,写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id,JSONUtil.toJsonStr(shop),CACHE_SHOP_TTL, TimeUnit.MINUTES);
        return Result.ok(shop);
    }

缓存雪崩

缓存击穿



使用互斥锁解决缓存击穿问题

    //解决缓存穿透
    public Shop querybyIdWithMutex(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断店铺是否存在
        if (StrUtil.isNotBlank(shopJson)) {
            //3.存在,直接返回
            return JSONUtil.toBean(shopJson, Shop.class);
        }
        if (shopJson != null) {
            //缓存返回空
            return null;
        }
        //4.1 获取互斥锁
        String lockKey = "lock:shop:" + id;
        Shop shop = null;
        try {
            //4.实现缓存重建

            boolean isLock = tryLock(lockKey);
            //4.2 判断是否获取成功
            if (!isLock) {
                //4.3失败则休眠开始递归
                Thread.sleep(50);
                return querybyIdWithMutex(id);
            }
            //4.3.成功根据id查询数据库
            shop = getById(id);
            //5.不存在直接返回错误
            if (shop == null) {
                stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, "", CACHE_NULL_TTL, TimeUnit.MINUTES);
                return null;
            }
            //6.不存在,写入redis
            stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY + id, JSONUtil.toJsonStr(shop), CACHE_SHOP_TTL, TimeUnit.MINUTES);

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //7 释放互斥锁
            undelet(lockKey);
        }

        return shop;
    }

添加redis线程锁

    //添加锁
    private boolean tryLock(String key) {
        // 设置redis的分布锁
        Boolean flag = stringRedisTemplate.opsForValue().setIfAbsent(key, "1", 10, TimeUnit.MINUTES);
        return BooleanUtil.isTrue(flag);
    }

    //删除锁
    private void undelet(String key) {
        stringRedisTemplate.delete(key);

    }

逻辑过期方式

 private static final ExecutorService CACHE_REDIIS=Executors.newFixedThreadPool(10);
    //逻辑过期时间解决缓存穿透
    public Shop querybyIdWithExpire(Long id) {
        //1.从redis查询商铺缓存
        String shopJson = stringRedisTemplate.opsForValue().get(CACHE_SHOP_KEY + id);
        //2.判断店铺是否存在
        if (StrUtil.isNotBlank(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缓存重建
        //获取互斥锁
        String lockKey = LOCK_SHOP_KEY+ id;
        boolean isLock = tryLock(lockKey);
        //6.2判断是否获取锁成功
        if (isLock) {
            //6.3成功开启独立线程
            CACHE_REDIIS.submit(()->{
                this.saveShop2Redis(id,20l);
                //释放锁
                undelet(lockKey);
            });
        }
        return shop;
    }

    private 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));
        //写入redis
        stringRedisTemplate.opsForValue().set(CACHE_SHOP_KEY+id, JSONUtil.toJsonStr(redisData));

    }

封装redis工具类

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

    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实现优惠券秒杀

全局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.生成时间戳
        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;
    }
}

实现优惠券下单效果

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.time.LocalDateTime;
@Service
public class IVoucherOrderServiceImpl 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("秒杀尚未开始");
        }
         if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
             return Result.fail("秒杀结束");
         }
         //4.判断库存
        if (voucher.getStock()<1){
            //库存不足
            return  Result.fail("库存不足");
        }
        //5.扣减库存
        boolean succes = seckillVoucherService.update().setSql("stock=stock - 1").eq("voucher_id", voucherId).update();
        if (!succes){
           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);
    }
}

解决优惠券超卖问题

在之前的写法高并发的场景下库存会出现超卖的问题。

乐观锁是用来解决更新数据时候的并发问题,

使用乐观锁解决超卖

使用CAS乐观锁解决

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
public class IVoucherOrderServiceImpl 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("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀结束");
        }
        //4.判断库存
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足");
        }
        //5.扣减库存
        boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
                .eq("voucher_id", voucherId)
                //判断跟我查到到值跟数据库到值是否一致
                .gt("stock", 0)
                .update();
        if (!succes) {
            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);
    }
}

实现一人一单

        //一人一单
        Long user_id = UserHolder.getUser().getId();
        //查询订单
        Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();
        //判断是否存在
        if (count > 0) {
            return Result.fail("购买过啦");
        }

但是会有超卖问题

使用悲观锁解决一人一单超卖问题

package com.hmdp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmdp.dto.Result;
import com.hmdp.entity.SeckillVoucher;
import com.hmdp.entity.User;
import com.hmdp.entity.VoucherOrder;
import com.hmdp.mapper.SeckillVoucherMapper;
import com.hmdp.mapper.VoucherOrderMapper;
import com.hmdp.service.ISeckillVoucherService;
import com.hmdp.service.IVoucherOrderService;
import com.hmdp.utils.RedisIdWorker;
import com.hmdp.utils.UserHolder;
import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sun.awt.AppContext;

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

@Service
public class IVoucherOrderServiceImpl extends ServiceImpl<VoucherOrderMapper, VoucherOrder> implements IVoucherOrderService {

    @Resource
    private ISeckillVoucherService seckillVoucherService;
    @Resource
    private RedisIdWorker redisIdWorker;

    @Override

    public Result seckillVoucher(Long voucherId) {
        //1,查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀结束");
        }
        //4.判断库存
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足");
        }
        //5.扣减库存
        boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
                .eq("voucher_id", voucherId)
                //判断跟我查到到值跟数据库到值是否一致
                .gt("stock", 0)
                .update();
        if (!succes) {
            return Result.fail("扣减失败");
        }
        //一人一单
        Long user_id = UserHolder.getUser().getId();
        //指定锁用户id,用户的id值一样的时候锁就一样,添加啦intern就不会每次都创建一个新的对象
        //先获取锁
        synchronized (user_id.toString().intern()) {
            //如果不添加动态代理会导致当前事物不生效
            IVoucherOrderService proxy = (IVoucherOrderService)AopContext.currentProxy();
            return proxy.creatVoutcherOrder(voucherId);
        }
        //等事物提交完在来释放锁


    }

    //添加悲观锁
    @Transactional
    public synchronized Result creatVoutcherOrder(Long voucherId) {
        //一人一单
        Long user_id = UserHolder.getUser().getId();
        //指定锁用户id,用户的id值一样的时候锁就一样,添加啦intern就不会每次都创建一个新的对象

        //查询订单
        Integer count = query().eq("user_id", user_id).eq("voucher_id", voucherId).count();
        //判断是否存在
        if (count > 0) {
            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);


    }
}

导入依赖

        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.2</version>
        </dependency>

启动类添加动态代理

package com.hmdp;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@MapperScan("com.hmdp.mapper")
@EnableAspectJAutoProxy(exposeProxy  = true)
@SpringBootApplication
public class HmDianPingApplication {

    public static void main(String[] args) {
        SpringApplication.run(HmDianPingApplication.class, args);
    }

}

分布式锁




基于redis实现分布锁

package com.hmdp.utils;

public interface ILock {

    /**
     * 尝试获取锁
     * @param timeoutSec 锁持有的超时时间,过期后自动释放
     * @return true代表获取锁成功; false代表获取锁失败
     */
    boolean tryLock(long timeoutSec);

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

分布式锁工具类

package com.hmdp.utils;

import cn.hutool.core.lang.UUID;
import org.springframework.core.io.ClassPathResource;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;

import java.util.Collections;
import java.util.concurrent.TimeUnit;

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) + "-";
    private static final DefaultRedisScript<Long> UNLOCK_SCRIPT;
    static {
        UNLOCK_SCRIPT = new DefaultRedisScript<>();
        UNLOCK_SCRIPT.setLocation(new ClassPathResource("unlock.lua"));
        UNLOCK_SCRIPT.setResultType(Long.class);
    } 

    @Override
    public 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() {
        // 调用lua脚本
        stringRedisTemplate.execute(
                UNLOCK_SCRIPT,
                Collections.singletonList(KEY_PREFIX + name),
                ID_PREFIX + Thread.currentThread().getId());
    }
    /*@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);
        }
    }*/
}

修改业务代码

 public Result seckillVoucher(Long voucherId) {
        //1,查询优惠券
        SeckillVoucher voucher = seckillVoucherService.getById(voucherId);
        //2.判断秒杀是否开始
        if (voucher.getBeginTime().isAfter(LocalDateTime.now())) {
            //尚未开始
            return Result.fail("秒杀尚未开始");
        }
        if (voucher.getEndTime().isBefore(LocalDateTime.now())) {
            return Result.fail("秒杀结束");
        }
        //4.判断库存
        if (voucher.getStock() < 1) {
            //库存不足
            return Result.fail("库存不足");
        }
        //5.扣减库存
        boolean succes = seckillVoucherService.update().setSql("stock=stock - 1")
                .eq("voucher_id", voucherId)
                //判断跟我查到到值跟数据库到值是否一致
                .gt("stock", 0)
                .update();
        if (!succes) {
            return Result.fail("扣减失败");
        }
        //一人一单
        Long user_id = UserHolder.getUser().getId();
        //创建锁对象
        SimpleRedisLock simpleRedisLock = new SimpleRedisLock("order:" + user_id, stringRedisTemplate);
        //获取锁
        boolean isLock = simpleRedisLock.tryLock(1200);
        //判断是否获取锁成功
        if (!isLock) {
            //获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.creatVoutcherOrder(voucherId);
        }finally {
            simpleRedisLock.unlock();
        }


        //等事物提交完在来释放锁


    }

极端情况下的误杀问题解决

业务阻塞后会出现极端情况,一个线程的锁超时释放后,这个进程结束阻塞的同时也会把另一个线程的锁给释放掉了。

解决方案,存放一个线程标识,是自己的锁才能释放不释放别人的锁。


    /*@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);
        }
    }*/

redis的原子性

使用Redisson对分布式锁进行优化

Redisson 入门

导入依赖

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

创建redissonconfig配置文件

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 RdissonConfig {
    @Bean
    public RedissonClient redissonClient(){
        //配置
        Config config = new Config();
        config.useSingleServer().setAddress("redis://102.129.195.102:6379").setPassword("bxr0814");
        //创建redissonclient对象
        return Redisson.create(config);
    }
}

接入redisson

省略之前代码
      //一人一单
        Long user_id = UserHolder.getUser().getId();
        //创建锁对象 初始化redisson
        RLock lock = redissonClient.getLock("lick:order:" + user_id);
        //获取锁 无参表示失败立即返回
        boolean isLock = lock.tryLock();
        //判断是否获取锁成功
        if (!isLock) {
            //获取锁失败,返回错误或重试
            return Result.fail("不允许重复下单");
        }

        try {
            IVoucherOrderService proxy = (IVoucherOrderService) AopContext.currentProxy();
            return proxy.creatVoutcherOrder(voucherId);
        }finally {
            lock.unlock();
        }

Rdisson可重入锁原理

使用map结构来计算同一个线程进入了多少次锁。

详细教程

https://blog.csdn.net/daobuxinzi/article/details/137187008

优化秒杀业务功能

目前都是同步进行的,优化方案是使用异步。使用独立的线程来解决耗时的部分。

新增优惠券的同时将优惠券信息保存到redis中

 // 保存秒杀库存到Redis中
        stringRedisTemplate.opsForValue().set(SECKILL_STOCK_KEY + voucher.getId(), voucher.getStock().toString());

基于lua脚本判断秒杀库存一人一单。


开启线程任务实现异步下单

// com.star.redis.dzdp.service.impl.VoucherOrderServiceImpl

/** 异步执行下单动作的线程池 */
private static final ExecutorService SECKILL_ORDER_EXECUTOR =
        Executors.newSingleThreadExecutor();

/** 类初始化之后立即初始化线程池 */
@PostConstruct
private void init() {
    SECKILL_ORDER_EXECUTOR.submit(new VoucherOrderHandler());
}

/**
 * 处理订单的内部类
 */
private class VoucherOrderHandler implements Runnable {
    @Override
    public void run() {
        // while循环持续读取队列中的信息
        while (true) {
            try {
                log.info("=====begin=====>");
                // 1.获取队列中的订单信息
                VoucherOrder voucherOrder = orderTasks.take();
                log.info("get from queue : {}", voucherOrder.toString());
                // 2.创建订单
                handleVoucherOrder(voucherOrder);
                log.info("=====end=====>");
            } catch (Exception e) {
                log.error("处理异常订单", e);
            }
        }
    }

    /**
     * 处理订单
     */
    private void handleVoucherOrder(VoucherOrder voucherOrder) {
        // 1.创建锁对象
        RLock rLock = redissonClient.getLock("lock:order:" + voucherOrder.getUserId());
        // 2.尝试获取锁
        boolean isLock = rLock.tryLock();
        log.info("isLock = {}", isLock);
        // 3.判断是否获取锁成功
        if(!isLock) {
            // 获取锁失败
            log.error("不允许重复下单!");
            return;
        }
        try {
            // 4.持锁真正创建订单
            checkAndCreateVoucherOrder(voucherOrder.getVoucherId(), voucherOrder.getUserId());
        } finally {
            // 5.释放锁
            rLock.unlock();
            log.info("unlock done.");
        }
    }
    
    /**
     * 持锁真正创建订单
     */
    private void createVoucherOrder(VoucherOrder voucherOrder) {
        log.info("begin createVoucherOrder... voucherId = {}, userId = {}, orderId = {}",
                voucherOrder.getVoucherId(), voucherOrder.getUserId(), voucherOrder.getId());
        // 1.增加一人一单规则
        int count = query().eq("voucher_id", voucherOrder.getVoucherId())
                .eq("user_id", voucherOrder.getUserId()).count();
        log.info("old order count = {}", count);
        if(count > 0) {
            // 该用户已下过单
            log.error("每个帐号只能抢购一张优惠券!");
            return;
        }
        // 2.扣减库存
        boolean update = seckillVoucherService.update().setSql("stock = stock - 1")
                .eq("voucher_id", voucherOrder.getVoucherId())
                .gt("stock", 0)
                .update();
        log.info("update result = {}", update);
        if(!update) {
            // 扣减库存失败,返回抢券失败
            log.error("库存不足,抢券失败!");
            return;
        }
        // 3.创建订单
        voucherOrder.setPayTime(new Date());
        voucherOrderService.save(voucherOrder);
    }
}

Redis的消息队列

Redis实现点赞一人一赞功能

//点赞接口

    @Override
    public Result likeBlog(Long id) {
        //1.获取登陆用户
        Long userId = UserHolder.getUser().getId();
        //2.判断当前登陆用户是否已经点赞
        String key =  "blog:liked:"+ id;
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        if (BooleanUtil.isFalse(isMember)) {
            //3.如果未点赞,可以点赞
            //3.1 数据库点赞+1
            boolean isSuccess = update().setSql("liked=liked+1").eq("id", id).update();
            //3.2保存用户刀redis的set集合
            if (isSuccess){
                stringRedisTemplate.opsForSet().add(key,userId.toString());
            }
        }else {
            //4.如果已点赞 取消点赞
            //4.1 数据库点赞-1
            boolean isSuccess = update().setSql("liked=liked-1").eq("id", id).update();
            stringRedisTemplate.opsForSet().remove(key,userId.toString());
              //3.2保存用户刀redis的set集合
            if (isSuccess){
                stringRedisTemplate.opsForSet().add(key,userId.toString());
            }
        }
        return Result.ok();
    }

//判断是否点赞

    private void isBlogLiked(Blog blog) {
         //1.获取登陆用户
        Long userId = UserHolder.getUser().getId();
        //2.判断当前登陆用户是否已经点赞
        String key =  "blog:liked:"+ blog.getId();
        Boolean isMember = stringRedisTemplate.opsForSet().isMember(key, userId.toString());
        boolean aTrue = BooleanUtil.isTrue(isMember);
        blog.setIsLink(aTrue);
    }


    @Override
    public Result queryHotBlog(Integer current) {
         // 根据用户查询
        Page<Blog> page = query()
                .orderByDesc("liked")
                .page(new Page<>(current, SystemConstants.MAX_PAGE_SIZE));
        // 获取当前页数据
        List<Blog> records = page.getRecords();
        // 查询用户
        records.forEach(blog ->{
            //查询用户信息
            this.queryBlogUser(blog);
            //判断是否点赞
            this.isBlogLiked(blog);
        });
        return Result.ok(records);
    }

Redis记录用户签到以及统计用户签到连续天数

    @Override
    public Result sign() {
        //1.获取当前登陆用户
        Long userId = 1010l;
        //2.获取日期
        LocalDateTime now = LocalDateTime.now();
        //3.拼接key
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        String key = "user_sing_" + userId + keySuffix;
        //4.获取今天是本月的第几天
        int dayOfMonth = now.getDayOfMonth();
        //5.写入redis setbit key offset 1
        stringRedisTemplate.opsForValue().setBit(key,dayOfMonth-1,true);
        return Result.ok();
    }

    @Override
    public Result signCount() {
 // 获取当前登录用户
        Long userId = 1010l;
        // 获取当前日期
        LocalDateTime now = LocalDateTime.now();
        String keySuffix = now.format(DateTimeFormatter.ofPattern(":yyyyMM"));
        // 拼接 key
        String key = "user_sing_" + userId + keySuffix;
        // 获取今天是本月的第几天
        // 当前日期为 2022年5月12号,故而 dayOfMonth = 12
        int dayOfMonth = now.getDayOfMonth();
        // 获取从 0 到 dayOfMonth 的签到结果,返回的是个十进制的数字
        List<Long> result = stringRedisTemplate.opsForValue().bitField(key,
                //redis的子命令,从当前日期开始到0开始查
                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);
        }

        int count = 0;
        // 循环遍历
        while(true){
            // 让这个数字与 1 做与运算,得到数字的最后一位 bit 位  判断这个 bit 位是否为0
            // num & 1 做与运算,其中 1 的左边以 0 补齐
            if ((num & 1) == 0) {
                // 如果为 0,说明未签到,结束
                break;
            } else {
                // 如果不为 0,说明已签到,计数器 +1
                count++;
            }
            // 把数字右移一位,抛弃最后一个 bit 位,继续下一个 bit 位
            // >> :右移 最高位是0,左边补齐0;最高为是1,左边补齐1
            // << :左移 左边最高位丢弃,右边补齐0
            // >>>:无符号右移 无论最高位是0还是1,左边补齐0
            // num >>>= 1 ------------> num = num >>> 1
            num >>>= 1;
        }
        return Result.ok(count);

    }
相关推荐
xiaoyalian4 小时前
R语言绘图过程中遇到图例的图块中出现字符“a“的解决方法
笔记·r语言·数据可视化
Red Red5 小时前
网安基础知识|IDS入侵检测系统|IPS入侵防御系统|堡垒机|VPN|EDR|CC防御|云安全-VDC/VPC|安全服务
网络·笔记·学习·安全·web安全
贰十六6 小时前
笔记:Centos Nginx Jdk Mysql OpenOffce KkFile Minio安装部署
笔记·nginx·centos
知兀6 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员
Natural_yz7 小时前
大数据学习17之Spark-Core
大数据·学习·spark
qq_172805597 小时前
RUST学习教程-安装教程
开发语言·学习·rust·安装
一只小小汤圆7 小时前
opencascade源码学习之BRepOffsetAPI包 -BRepOffsetAPI_DraftAngle
c++·学习·opencascade
醉陌离7 小时前
渗透测试笔记——shodan(4)
笔记
虾球xz7 小时前
游戏引擎学习第20天
前端·学习·游戏引擎
LateBloomer7778 小时前
FreeRTOS——信号量
笔记·stm32·学习·freertos