Redis缓存整合

在高并发的环境下数据库成为系统的短板,所以引入缓存,作为数据库前的一道防线,避免所有的请求直接走数据库,降低数据库的压力,数据库层只承担“能力范围内”的访问请求。

由于一个项目中有不同模块的功能,所以在Redis封装时需要创建一个业务前缀拼接在Key前面,便于区分各个模块。

前缀接口:

/**
 * Key的前缀接口
 * 用来区分各个模块
 */
public interface KeyPrefix {
    /**
     * 过期时间
     * @return
     */
    int expireSeconds();

    /**
     * get前缀
     * @return
     */
    String getPrefix();
}

抽象类

@Data
@AllArgsConstructor
@NoArgsConstructor
public abstract class BasePrefix implements KeyPrefix {
    private int expireSeconds;
    private String prefix;

    /**
     * 0默认Key永不过期
     * @return
     */
    public BasePrefix(String prefix){
        this.expireSeconds = 0;
        this.prefix = prefix;
    }

    @Override
    public int expireSeconds() {
        return expireSeconds;
    }

    @Override
    public String getPrefix() {
        return getClass().getSimpleName()+":"+prefix;
    }
}

RedisService 封装常用的方法,这里只使用了String类型的key-value:

@Service
public class RedisService {
    @Autowired
    private JedisPool jedisPool;

    /**
     * get 值
     * @param prefix 模块前缀
     * @param key key
     * @param classzz key对应值的类型
     * @param <T>
     * @return
     */
    public <T> T get(KeyPrefix prefix,String key,Class<T> classzz) {
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            //key 拼接为前缀-key
            key = prefix.getPrefix() + "-" + key;
            String s = jedis.get(key);
            T t = StringToBean(s, classzz);
            return t;
        }finally {
            returnToPool(jedis);
        }
    }


    /**
     * set值
     * @param prefix 模块前缀
     * @param key key
     * @param value 值
     * @param <T>
     * @return
     */
    public <T> Boolean set(KeyPrefix prefix,String key,T value){
        Jedis jedis = null;
        try{
            jedis = jedisPool.getResource();
            //将需要set的值转为字符串
            String s = beanToString(value);
            if(s ==null || s.length() <=0){
                return false;
            }
            //key 拼接为前缀-key
            key = prefix.getPrefix() +"-"+ key;
            int expireSeconds = prefix.expireSeconds();
            //如果未设置过期时间 也就是默认为0
            if(expireSeconds <= 0){
                jedis.set(key, s);
            }else {
                jedis.setex(key,expireSeconds,s);
            }
            return true;
        }finally {
            returnToPool(jedis);
        }
    }

    /**
     * key 是否存在
     * @param prefix
     * @param key
     * @return
     */
    public Boolean exist(KeyPrefix prefix,String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            key = prefix.getPrefix() + "-" + key;
            Boolean exists = jedis.exists(key);
            return exists;
        }finally {
            returnToPool(jedis);
        }
    }

    /**
     * 自增一
     * @param prefix
     * @param key
     * @return
     */
    public Long incr(KeyPrefix prefix,String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            key = prefix.getPrefix() + "-" + key;
            Long incr = jedis.incr(key);
            return incr;
        }finally {
            returnToPool(jedis);
        }
    }

    /**
     * 自减一
     * @param prefix
     * @param key
     * @return
     */
    public Long decr(KeyPrefix prefix,String key){
        Jedis jedis = null;
        try {
            jedis = jedisPool.getResource();
            key = prefix.getPrefix() + "-" + key;
            Long incr = jedis.decr(key);
            return incr;
        }finally {
            returnToPool(jedis);
        }
    }

    public void returnToPool(Jedis jedis){
        if(jedis != null){
            System.out.println("returnToPool");
            jedis.close();
        }
    }

    /**
     * 类型转字符串
     * @param value 需要转字符串的类型
     * @param <T>
     * @return
     */
    public <T> String beanToString(T value){
        Class<?> aClass = value.getClass();
        //判空
        if(value == null){
            return null;
        }
        if(aClass == Integer.class || aClass == int.class){
            return ""+value;
        }else if(aClass == String.class){
            return (String)value;
        }else if(aClass == long.class || aClass == Long.class){
            return ""+value;
        }else{
            return JSON.toJSONString(value);
        }
    }

    /**
     * 字符串转类型
     * @param str 字符串
     * @param classzz 类型
     * @param <T>
     * @return
     */
    public <T> T StringToBean(String str,Class<T> classzz){
        //判空
        if(str ==  null || str.length()<=0 || classzz == null){
            return null;
        }
        if(classzz == Integer.class || classzz == int.class){
            return (T) Integer.valueOf(str);
        }else if(classzz == String.class){
            return (T)str;
        }else if(classzz == long.class || classzz == Long.class){
            return (T) Long.valueOf(str);
        }else{
            return JSON.toJavaObject(JSON.parseObject(str),classzz);
        }
    }
}

因为存储的keyvalue都是String类型,所以在存取时要对bean对象进行转换。


封装MD5

public class Md5Utils {
    public static final String salt = "1a2b3c4d";
    public static String md5(String str){
        return DigestUtils.md5Hex(str);
    }

    /**
     * 第一次对表单输入的密码进行MD5+salt
     * @param password 表单输入的密码
     * @return
     */
    public static String passToForm(String password){
        password = ""+salt.charAt(0)+salt.charAt(2)+password+salt.charAt(4)+salt.charAt(5);
        return Md5Utils.md5(password);
    }

    /**
     * 第二次对第一次加密的密码进行第二次加密
     * @param formPass 第一次加密后的密码
     * @param salt 随机生成
     * @return
     */
    public static String formPassToDb(String formPass,String salt){
        formPass = ""+salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(4)+salt.charAt(5);
        return Md5Utils.md5(formPass);
    }

    /**
     * 将用户输入的密码经过两次md5加密
     * @param pass
     * @param dbSalt
     * @return
     */
    public static String passToDb(String pass,String dbSalt){
        return Md5Utils.formPassToDb(Md5Utils.passToForm(pass),dbSalt);
    }

    public static void main(String[] args) {
        System.out.println(Md5Utils.passToForm("123456"));
    }
}

两次MD5登录加密

增加安全性,增大反查难度

两次MD5

该流程采用两次MD5加密,第一次加密在前端,固定一个salt,对于用户传入过来的明文密码加密,防止明文密码在网络中传输。

function login() {
        var pass = $("#password").val();
        var salt = "1a2b3c4d";
        var password = salt.charAt(0)+salt.charAt(2)+pass+salt.charAt(4)+salt.charAt(5);
        var pass = $.md5(encodeURIComponent(password));
        $.ajax({
            url:"/dologin",
            type:"POST",
            data:{
                nickname:$("#nickname").val(),
                password:pass
            },
            success:function (data) {
                if(data.code == 0){
                    alert("登陆成功");
                    window.location.href = "/togoodslist";
                }else{
                    alert("登陆失败,请检查账号是否正确")
                }
            },
        })
    }

第二次MD5

    @ResponseBody
    @RequestMapping("/dologin")
    public Result<String> doLogin(String nickname,String password,HttpServletResponse response){
        User loginUser = userDao.selectUserByNickName(nickname);
        if(loginUser == null){
            return Result.error(CodeMsg.NICKNAME_NOT_EXIST);
        }
        String salt = loginUser.getSalt();
        //将表单提交的密码二次MD5
        String s = Md5Utils.formPassToDb(password, salt);
        //验证密码
        if(s.equals(loginUser.getPassword())){
            //生成随机的token
            String token = UUIDUtil.uuid();
            //将token和用户作为key与value存入缓存
            redisService.set(MiaoSha_UserKey.userKeyToken,token,loginUser);
            //封装cookie
            Cookie cookie = new Cookie(COOKIE_NAME,token);
            //设置cookie过期时间
            cookie.setMaxAge(MiaoSha_UserKey.userKeyToken.getExpireSeconds());
            cookie.setPath("/");
            response.addCookie(cookie);
            return Result.success(token);
        }
        return Result.error(CodeMsg.SERVER_ERROR);
    }

第二次md5根据前端传过来的已经被加密过的密码进行二次加密,此时salt可随机生成,数据库中创建一个字段保存,登录验证时比较两次加密过后密码是否与数据库中相同,登录成功后进行基于Redis+Cookiesession共享。


页面、对象缓存

页面缓存多用于变化不明显的页面,如商品列表等等

@RequestMapping(value = "/togoodslist",produces = "text/html")
    @ResponseBody
    public String toGoodsList(HttpServletRequest request, HttpServletResponse response, Model model, @UserParameter User user){
        //取缓存
        String html = redisService.get(GoodListKey.goodListKey, "goodList", String.class);
        if(!StringUtils.isEmpty(html)){
            System.out.println("拿出页面缓存");
            return html;
        }

        model.addAttribute("loginUser",user);
        List<GoodsList> goodsList = goodsService.getGoodsList();
        model.addAttribute("goodsList",goodsList);

        //缓存中没有,手动渲染
        WebContext webContext = new WebContext(request,response,request.getServletContext(),request.getLocale(),model.asMap());                                     
        html = thymeleafViewResolver.getTemplateEngine().process("goods_list",webContext);
        if(!StringUtils.isEmpty(html)){
            //装入缓存
            System.out.println("页面存入缓存");
            redisService.set(GoodListKey.goodListKey,"goodList",html);
        }
        return html;
    }

这里页面使用的Thymeleaf,进入请求后首先查找对应的缓存,若缓存中有就直接返回html,若没有,则进行手动渲染成String类型的Html存入redis,过期时间一般设置为60s,用户再次刷新则直接走redis,避免了一些操作数据库和渲染的流程。

对象缓存粒度较细,作用同样是绕开访问数据库,减轻数据库的压力

@RequestMapping("/miaosha_static")
    @ResponseBody
    public Result<Order_info> miaoSha_static(@RequestParam("good_id") int good_id, @UserParameter User user){
        GoodsList good = goodsService.getGoodsListById(good_id);
        //判断该商品是否还有库存
        int stock_count = miaoSha_goodsService.getMiaosha_goodsByid(good.getId());
        if(stock_count <= 0){
            return Result.error(CodeMsg.OUT_OF_STOCK);
        }

        //判断该用户是第一次秒杀,不可重复秒杀
        //查找redis缓存,绕开数据库
        Miaosha_order miaosha_order = redisService.get(MiaoSha_OrderKey.orderKey,user.getId()+":"+good.getId(),Miaosha_order.class);
        if(miaosha_order != null){
            return Result.error(CodeMsg.NO_REPEAT_MIAOSHA);
        }

        //允许秒杀
        //库存-1 生成order_info订单 生成秒杀订单
        int i = miaoShaService.miaoSha(user, good);
        //查找订单
        Order_info order_info = order_infoService.selectOrderById(i);
        return Result.success(order_info);
    }

用户成功秒杀到商品后生成的订单自动存入缓存,使用用户的id和商品的id作为key

        //订单存入缓存
        redisService.set(MiaoSha_OrderKey.orderKey,user.getId()+":"+good.getId(),miaosha_order);

页面静态化

也就是所谓前后端分离,由于VUE不熟,这里用JQ模拟。

function getDetail () {
        var goodid = g_getQueryString("goodsId");
        $.ajax({
            url: "goodsdetail_static/"+goodid,
            type: "GET",
            success:function (data) {
                if(data.code == 0){
                    render(data.data)
                }else{
                    alert("服务端请求错误")
                }
            },
            error:function () {
                alert("服务端请求错误")
            }
        })
    }

    function render(goodDetail) {
        var second = goodDetail.second;
        var status = goodDetail.status;
        var good = goodDetail.good;

        $("#goods_name").text(good.goods_name);
        $("#goods_img").attr("src",good.goods_img);
        $("#goods_detail").text(good.goods_detail);
        $("#goods_price").text(good.goods_price);
        $("#miaosha_price").text(good.miaosha_price);
        $("#stock_count").text(good.stock_count);
        $("#start_date").text(new Date(good.start_date).format("yyyy-MM-dd hh:mm:ss"));
        $("#timeSecond").val(second);
        $("#status").val(status);
        $("#good_id").val(good.id);
        fun();
    }

    $(function () {
        getDetail()
    })

后端接口返回Json:
data

将动态页面转化为实际存在的静态页面这种方法,由于静态页面的存在,少了动态解析过程,所以提高了页面的访问速度和稳定性,使得优化效果非常明显。

最后修改:2021 年 03 月 24 日 02 : 46 PM
如果觉得我的文章对你有用,请随意赞赏