公众号上线啦!
搜一搜【国服冰】
使命:尽自己所能给自学后端开发的小伙伴提供一个少有弯路的平台
回复:国服冰,即可领取我为大家准备的资料,里面包含整体的Java学习路线,电子书,以及史上最全的面试题!
一、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);
}
}
}
因为存储的key
与value
都是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
加密,第一次加密在前端,固定一个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
+Cookie
的session
共享。
四、页面、对象缓存
页面缓存多用于变化不明显的页面,如商品列表等等
@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:
将动态页面转化为实际存在的静态页面这种方法,由于静态页面的存在,少了动态解析过程,所以提高了页面的访问速度和稳定性,使得优化效果非常明显。