.NET单点登录实现方法----两种 - 其实丿很简单 - 博客园

mikel阅读(165)

来源: .NET单点登录实现方法—-两种 – 其实丿很简单 – 博客园

第一种模式:同一顶级域名下cookie共享,代码如下

1
2
3
4
5
6
HttpCookie cookies = new HttpCookie("Token");
cookies.Expires = DateTime.Now.AddDays(7);
cookies.Path = "/";
cookies.Domain = "xxxx.com";//设置同一顶级域名
cookies.Value = "token的值";
System.Web.HttpContext.Current.Response.Cookies.Add(cookies);

 

第二种模式:跨域下实现单点登录

IIS 搭建https站点_iis 新建443-CSDN博客

mikel阅读(151)

来源: IIS 搭建https站点_iis 新建443-CSDN博客

1. 新建自签名证书

 

2. 新建一个https站点,type选https, 端口号默认443,也可以更改。证书下拉选择上一步创建的那个.hostname填写自己想要的。高版本iis可以直接填写host name. 低版本需要修改,用图二方法

 

 

 

3. 创建好后,选中网站,点击右栏的ssl settings. 操作如下。这样新建的站点就不会弹出是否继续的安全提示

 

 

4. 然后host里配置好域名指向本机。至此,自搭建https 站点就此完成。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/flyingshuai/article/details/105795847

本地PostMan双击无响应解决方法_为什么双击了postman转了一下就关掉了-CSDN博客

mikel阅读(424)

来源: 本地PostMan双击无响应解决方法_为什么双击了postman转了一下就关掉了-CSDN博客

解决步骤
本地安装postman后,一直使用正常,现突然双击无响应,网上查了一些解决方法,可能是我本地自动更新导致此问题,以下是我解决的过程。猜想可能是高版本不稳定,或者更新过程文件有缺失。

(1)通过桌面快捷方式找到postman的exe启动文件的路径
(2)选择低版本文件夹里面的启动文件

(3)启动正常后,通过File==>settings==>Update 关闭自动更新

————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/qq_38820345/article/details/109547494

未能加载文件或程序集“Newtonsoft.Json”或它的某一个依赖项。找到的程序集清单 --解决-CSDN博客

mikel阅读(285)

来源: 未能加载文件或程序集“Newtonsoft.Json”或它的某一个依赖项。找到的程序集清单 –解决-CSDN博客

一:页面显示如下错误

 

二 运行时显示

 

三:在web.config中修改Newtonsoft.Json.dll版本

出错前显示如下:

 

找到Newtonsoft.Json.dll引用的版本

然后修改成当前版本

 

然后保存web.config 运行即可。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/mym2018/article/details/82143781

阿里云短信 - __破 - 博客园

mikel阅读(179)

来源: 阿里云短信 – __破 – 博客园

阿里云短信

需要创建Accesskey

 

需要创建签名 发送模板

调用api发送短信相关代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
package com.po.reggie.utils;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsRequest;
import com.aliyuncs.dysmsapi.model.v20170525.SendSmsResponse;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.profile.DefaultProfile;
/**
 * 短信发送工具类
 */
public class SMSUtils {
    /**
     * 发送短信
     * @param signName 签名
     * @param templateCode 模板
     * @param phoneNumbers 手机号
     * @param param 参数
     */
    public static void sendMessage(String signName, String templateCode,String phoneNumbers,String param){
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou""LTAI5tM6p--------MScq1gq""4fhp0t8j0y2-----UIlbgfH1GU");
        IAcsClient client = new DefaultAcsClient(profile);
        SendSmsRequest request = new SendSmsRequest();
        request.setSysRegionId("cn-hangzhou");
        request.setPhoneNumbers(phoneNumbers);
        request.setSignName(signName);
        request.setTemplateCode(templateCode);
        request.setTemplateParam("{\"code\":\""+param+"\"}");
        try {
            SendSmsResponse response = client.getAcsResponse(request);
            System.out.println("短信发送成功");
        }catch (ClientException e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        sendMessage("破土重生科技股份有限公司","SMS_154950909","13522715896","12345678");
    }
}

代码开发-导入maven坐标

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.16</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-dysmsapi</artifactId>
<version>2.1.0</version>
</dependency>

验证码的校验和登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
package com.po.reggie.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.po.reggie.common.R;
import com.po.reggie.domain.User;
import com.po.reggie.service.UserService;
import com.po.reggie.utils.ValidateCodeUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpSession;
import java.util.Map;
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    /**
     * 发送手机短信验证码
     * @param user
     * @return
     */
    @PostMapping("/sendMsg")
    public R<String> sendMsg(@RequestBody User user, HttpSession session){
        //获取手机号
        String phone = user.getPhone();
        if(StringUtils.isNotEmpty(phone)){
            //生成随机的4位验证码
            String code = ValidateCodeUtils.generateValidateCode(4).toString();
            log.info("code={}",code);
            //调用阿里云提供的短信服务API完成发送短信
            //SMSUtils.sendMessage("瑞吉外卖","",phone,code);
            //需要将生成的验证码保存到Session
            session.setAttribute(phone,code);
            return R.success("手机验证码短信发送成功");
        }
        return R.error("短信发送失败");
    }
    /**
     * 移动端用户登录
     * @param map
     * @param session
     * @return
     */
    @PostMapping("/login")
    public R<User> login(@RequestBody Map map, HttpSession session){
        log.info(map.toString());
        //获取手机号
        String phone = map.get("phone").toString();
        //获取验证码
        String code = map.get("code").toString();
        //从Session中获取保存的验证码
        Object codeInSession = session.getAttribute(phone);
        //进行验证码的比对(页面提交的验证码和Session中保存的验证码比对)
        if(codeInSession != null && codeInSession.equals(code)){
            //如果能够比对成功,说明登录成功
            LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
            queryWrapper.eq(User::getPhone,phone);
            User user = userService.getOne(queryWrapper);
            if(user == null){
                //判断当前手机号对应的用户是否为新用户,如果是新用户就自动完成注册
                user = new User();
                user.setPhone(phone);
                user.setStatus(1);
                userService.save(user);
            }
            session.setAttribute("user",user.getId());
            return R.success(user);
        }
        return R.error("登录失败");
    }
}

 

.Net对接阿里云短信平台 - 王精灵 - 博客园

mikel阅读(178)

来源: .Net对接阿里云短信平台 – 王精灵 – 博客园

一、在对接阿里云短信平台之前需完成阿里云短信平台短信签名和短信模板的配置,在此不做过多说明,建议采用企业账号的身份申请短信签名和短信模板更容易审核通过一些
阿里云短信平台地址 https://www.aliyun.com/product/sms?spm=5176.19720258.J_2686872250.7.7b812c4aBro5hF

二、完成以上配置之后在项目Nuget包中安装:AlibabaCloud.SDK.Dysmsapi20170525

三、主体代码

复制代码
        public void SendSms(string PhoneNumbers)
        {
            var verifyCode = GetVerifyCode();
            JObject jObject = new JObject();
            jObject.Add("code", verifyCode);
            AlibabaCloud.SDK.Dysmsapi20170525.Client client = CreateClient("accessKeyId", "accessKeySecret");
            AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest sendSmsRequest = new AlibabaCloud.SDK.Dysmsapi20170525.Models.SendSmsRequest
            {
                PhoneNumbers = PhoneNumbers,
                SignName = "SignName",
                TemplateCode = "TemplateCode",
                TemplateParam = jObject.ToString()
            };
            SendSmsResponse sendSmsResponse = client.SendSms(sendSmsRequest);
            if (sendSmsResponse.Body.Code == "OK" && sendSmsResponse.Body.Message == "OK")
            {
                textEdit1.Text = sendSmsResponse.Body.BizId;
                SetCacheTimeSpan(sendSmsResponse.Body.BizId, verifyCode, 20);
            }
            else if("isv.BUSINESS_LIMIT_CONTROL".Equals(sendSmsResponse.Body.Code))
            {
                throw new Exception("获取验证码过于频繁");
            }
        }


        public static AlibabaCloud.SDK.Dysmsapi20170525.Client CreateClient(string accessKeyId, string accessKeySecret)
        {
            AlibabaCloud.OpenApiClient.Models.Config config = new AlibabaCloud.OpenApiClient.Models.Config
            {
                AccessKeyId = accessKeyId,
                AccessKeySecret = accessKeySecret,
            };
            config.Endpoint = "dysmsapi.aliyuncs.com";
            return new AlibabaCloud.SDK.Dysmsapi20170525.Client(config);
        }


        /// <summary>
        /// 生成6位数随机验证码
        /// </summary>
        /// <returns></returns>
        private static int GetVerifyCode()
        {
            Random random = new Random();
            return random.Next(100000, 999999);
        }

        /// <summary>
        /// 设置缓存相对过期时间
        /// </summary>
        /// <param name="cacheKey">key</param>
        /// <param name="objValue">缓存对象</param>
        /// <param name="timeSpan">过期时间(秒)</param>
        public static void SetCacheTimeSpan(string cacheKey, object objValue, long timeSpan)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            objCache.Insert(cacheKey, objValue, null, DateTime.MaxValue, TimeSpan.FromSeconds(timeSpan));
        }


        /// <summary>
        /// 获取Cache的value
        /// </summary>
        /// <param name="cacheKey"></param>
        /// <returns></returns>
        public object GetCacheValue(string cacheKey)
        {
            System.Web.Caching.Cache objCache = HttpRuntime.Cache;
            return objCache.Get(cacheKey);
        }
复制代码

四、调用

            SendSms(PhoneNumbers);

Token,Session,Cookie,JWT,Oauth2傻傻分不清楚 - 苏三说技术 - 博客园

mikel阅读(160)

来源: Token,Session,Cookie,JWT,Oauth2傻傻分不清楚 – 苏三说技术 – 博客园

前言

最近发现有些小伙伴,对Token、Session、Cookie、JWT、OAuth2这些概念非常容易搞混。

有些小伙伴在工作中可能会遇到过这样的困惑:

  • 做登录功能时,到底该用Session还是JWT?
  • OAuth2和Token是什么关系?
  • 为什么有的方案要把Token存在Cookie里?

今天这篇文章专门跟大家一起聊聊这个话题,希望对你会有所帮助。

一、从餐厅就餐模型开始讲

为了让大家更好理解,我先用一个餐厅就餐的比喻来解释这些概念:

image

现在,让我们深入每个概念的技术细节。

二、Cookie:HTTP的世界身份证

2.1 什么是Cookie?

Cookie是存储在浏览器端的一小段文本数据,由服务器通过HTTP响应头的Set-Cookie字段发送给浏览器,浏览器随后会自动在每次请求中通过Cookie头将其带回给服务器。

工作原理

image

2.2 Cookie实战代码

// 服务器设置Cookie
@PostMapping("/login")
public ResponseEntity login(@RequestBody User user, HttpServletResponse response) {
    if (authService.authenticate(user)) {
        Cookie cookie = new Cookie("session_id", generateSessionId());
        cookie.setMaxAge(3600); // 1小时有效期
        cookie.setHttpOnly(true); // 防止XSS攻击
        cookie.setSecure(true); // 仅HTTPS传输
        cookie.setPath("/"); // 对整个站点有效
        response.addCookie(cookie);
        return ResponseEntity.ok().build();
    }
    return ResponseEntity.status(401).build();
}

// 读取Cookie
@GetMapping("/profile")
public ResponseEntity getProfile(@CookieValue("session_id") String sessionId) {
    User user = sessionService.getUserBySession(sessionId);
    return ResponseEntity.ok(user);
}

2.3 Cookie的重要属性

属性 作用 安全建议
HttpOnly 防止JavaScript访问 必须设置为true,防XSS
Secure 仅通过HTTPS传输 生产环境必须设置为true
SameSite 控制跨站请求时是否发送Cookie 建议设置为Strict或Lax
Max-Age 设置Cookie有效期 根据业务安全性要求设置

三、Session:服务端的用户档案

3.1 什么是Session?

Session是存储在服务器端的用户状态信息。服务器为每个用户创建一个唯一的Session ID,并通过Cookie将这个ID传递给浏览器,浏览器后续请求时带上这个ID,服务器就能识别用户身份。

Session存储结构

// 典型的Session数据结构
public class UserSession {
    private String sessionId;
    private String userId;
    private String username;
    private Date loginTime;
    private Date lastAccessTime;
    private Map<String, Object> attributes; // 自定义属性
    
    // 省略getter/setter
}

3.2 Session实战代码

// 基于Spring Session的实现
@PostMapping("/login")
public String login(@RequestParam String username, 
                   @RequestParam String password,
                   HttpSession session) {
    User user = userService.authenticate(username, password);
    if (user != null) {
        // 将用户信息存入Session
        session.setAttribute("currentUser", user);
        session.setAttribute("loginTime", new Date());
        return "redirect:/dashboard";
    }
    return "login?error=true";
}

@GetMapping("/dashboard")
public String dashboard(HttpSession session) {
    // 从Session获取用户信息
    User user = (User) session.getAttribute("currentUser");
    if (user == null) {
        return "redirect:/login";
    }
    return "dashboard";
}

3.3 Session的存储方案

1. 内存存储(默认)

# application.yml
server:
  servlet:
    session:
      timeout: 1800 # 30分钟过期时间

2. Redis分布式存储

@Configuration
@EnableRedisHttpSession // 启用Redis Session存储
public class SessionConfig {
    @Bean
    public LettuceConnectionFactory connectionFactory() {
        return new LettuceConnectionFactory();
    }
}

3. Session集群同步问题

image

四、Token:去中心化的身份令牌

4.1 什么是Token?

Token是一种自包含的身份凭证,服务器不需要在服务端存储会话状态,所有必要信息都包含在Token本身中。

Token vs Session 核心区别
image

4.2 Token实战代码

// 生成Token
public String generateToken(User user) {
    long currentTime = System.currentTimeMillis();
    return JWT.create()
            .withIssuer("myapp") // 签发者
            .withSubject(user.getId()) // 用户ID
            .withClaim("username", user.getUsername())
            .withClaim("role", user.getRole())
            .withIssuedAt(new Date(currentTime)) // 签发时间
            .withExpiresAt(new Date(currentTime + 3600000)) // 过期时间
            .sign(Algorithm.HMAC256(secret)); // 签名密钥
}

// 验证Token
public boolean validateToken(String token) {
    try {
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret))
                .withIssuer("myapp")
                .build();
        DecodedJWT jwt = verifier.verify(token);
        return true;
    } catch (JWTVerificationException exception) {
        return false;
    }
}

五、JWT:现代化的Token标准

5.1 什么是JWT?

JWT(JSON Web Token)是一种开放标准(RFC 7519),用于在各方之间安全地传输信息作为JSON对象。

这种信息可以被验证和信任,因为它是数字签名的。

JWT结构

header.payload.signature

解码示例

// Header
{
  "alg": "HS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022,
  "exp": 1516242622
}

// Signature
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

5.2 JWT实战代码

// 创建JWT
public String createJWT(User user) {
    return Jwts.builder()
            .setHeaderParam("typ", "JWT")
            .setSubject(user.getId())
            .setIssuer("myapp")
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + 3600000))
            .claim("username", user.getUsername())
            .claim("role", user.getRole())
            .signWith(SignatureAlgorithm.HS256, secret.getBytes())
            .compact();
}

// 解析JWT
public Claims parseJWT(String jwt) {
    return Jwts.parser()
            .setSigningKey(secret.getBytes())
            .parseClaimsJws(jwt)
            .getBody();
}

// 在Spring Security中使用JWT
@Component
public class JwtFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, 
                                  HttpServletResponse response, 
                                  FilterChain chain) {
        String token = resolveToken(request);
        if (token != null && validateToken(token)) {
            Authentication auth = getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        chain.doFilter(request, response);
    }
}

5.3 JWT的最佳实践

1. 安全存储

// 前端安全存储方案
// 不推荐:localStorage(易受XSS攻击)
// 推荐:HttpOnly Cookie(防XSS)或内存存储

2. 令牌刷新机制

// 双Token机制:Access Token + Refresh Token
public class TokenPair {
    private String accessToken;  // 短期有效:1小时
    private String refreshToken; // 长期有效:7天
}

// 刷新令牌接口
@PostMapping("/refresh")
public ResponseEntity refresh(@RequestBody RefreshRequest request) {
    String refreshToken = request.getRefreshToken();
    if (validateRefreshToken(refreshToken)) {
        String userId = extractUserId(refreshToken);
        String newAccessToken = generateAccessToken(userId);
        return ResponseEntity.ok(new TokenPair(newAccessToken, refreshToken));
    }
    return ResponseEntity.status(401).build();
}

六、OAuth 2.0:授权框架之王

6.1 什么是OAuth 2.0?

OAuth 2.0是一个授权框架,允许第三方应用在获得用户授权后,代表用户访问受保护的资源。

OAuth 2.0角色

  • 资源所有者(Resource Owner):用户
  • 客户端(Client):第三方应用
  • 授权服务器(Authorization Server):颁发访问令牌
  • 资源服务器(Resource Server):托管受保护资源

6.2 OAuth 2.0授权码流程

image

6.3 OAuth 2.0实战代码

// Spring Security OAuth2配置
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired
    private AuthenticationManager authenticationManager;
    
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("clientapp")
                .secret(passwordEncoder.encode("123456"))
                .authorizedGrantTypes("authorization_code", "refresh_token")
                .scopes("read", "write")
                .redirectUris("http://localhost:8080/callback");
    }
    
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                .tokenStore(tokenStore())
                .accessTokenConverter(accessTokenConverter());
    }
}

// 资源服务器配置
@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/private/**").authenticated()
                .antMatchers("/api/admin/**").hasRole("ADMIN");
    }
}

七、五大概念对比

为了让大家更清晰地理解这五个概念的关系和区别,我准备了以下对比表格:

7.1 功能定位对比

概念 本质 存储位置 主要用途 特点
Cookie HTTP状态管理机制 浏览器 维持会话状态 自动携带,有大小限制
Session 服务端会话信息 服务器 存储用户状态 服务端状态,需要存储管理
Token 访问凭证 客户端/服务端 身份认证 自包含,可验证
JWT Token的一种实现标准 客户端/服务端 安全传输信息 标准化,自包含,可签名
OAuth2 授权框架 不直接存储 第三方授权 标准化授权流程

7.2 应用场景对比

场景 推荐方案 原因说明
传统Web应用 Session + Cookie 简单易用,生态成熟
前后端分离应用 JWT 无状态,适合API认证
第三方登录 OAuth 2.0 标准化授权,安全可靠
微服务架构 JWT 分布式认证,无需会话同步
移动端应用 Token 轻量级,适合移动网络

7.3 安全考虑对比

安全威胁 Cookie方案防护 Token/JWT方案防护
XSS攻击 HttpOnly Cookie 避免localStorage存储
CSRF攻击 SameSite Cookie 自定义Header+CSRF Token
令牌泄露 短期有效+HTTPS 短期有效+HTTPS+刷新机制
数据篡改 服务端验证 签名验证

总结

通过今天的深入探讨,我们可以得出以下结论:

  1. Cookie是载体:HTTP协议的状态管理机制,是Session和Token的传输媒介之一。
  2. Session是状态:服务端维护的会话状态,需要借助Cookie或URL重写来实现。
  3. Token是凭证:认证授权的凭证,可以放在Cookie、Header或URL中。
  4. JWT是标准:Token的一种标准化实现,自包含、可验证、可信任。
  5. OAuth2是框架:授权框架,定义了完整的第三方授权流程。

最终建议

  • 简单Web应用:Session + Cookie
  • 前后端分离:JWT + HTTP Header
  • 第三方授权:OAuth 2.0 + JWT

没有最好的方案,只有最合适的方案。

理解每个技术的本质和适用场景,才能做出正确的架构决策。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

本文收录于我的技术网站http://www.susan.net.cn

实体框架 - 北在北方 - 博客园

mikel阅读(271)

来源: 实体框架 – 北在北方 – 博客园

实体框架的使用分为Model First,Code First(代码生成模型),Database First。Model First和Database First会使用实体设计器(edmx文件)来创建实体数据模型。

DbContext所使用的连接字符串如果是常规连接字符串则使用Code First,如果使用的是特殊的实体框架连接字符串,则使用Database First或Model First。

 

Database First:

从现有数据库构建edmx文件,然后edmx会生成DbContext和poco模型(model类),并将实体框架专用的连接字符串保存到所在项目的app.config中,但配置文件中的连接字符串并不能直接使用,连接字符串中包含有指向csdl,ssdl,msl文件的路径,自动生成的连接字符串中这3条路径包含一个“*”号,需将edmx所在类库的程序集名称替换“*”号。

Model First:

建立空的edmx文件,手动构建数据库模型,再由根据模型创建数据库和生成DbContext以及poco模型(model类)

Code First:

手写POCO,再由DbContext生成对应的数据库。

POCO中外键和导航属性应用virtual修饰(详情见导航属性的加载),以代理类便于重写。集合类型的导航属性应在构造方法中初始化为HashSet集合。

IDatabaseInitializer<TContext>(TContext是DbContext类型的实例),用于指定DbContext初次使用时用于初始化数据库的策略,使用策略须调用DbContext的DataBase属性(System.Data.Entity.Database实例)的SetInitializer方法,此初始化数据库策略在Global.asax的Application_Start方法中直接调用Database.SetInitializer静态方法,或者在DbContext的构造方法中执行Database.SetInitializer静态方法,或者在配置文件的entityFramwork/contexts的子节点配置,示例如下:

<entityFramework>

<contexts>

<context type=”[DbContext类],[程序集名称]”>

<databaseInitializer type=”[IDatabaseInitializer实现类],[程序集名称]” />

</context>

</contexts>

</entityFramework>

可使用的初始化策略有DropCreateDatabaseIfModelChanged<TContext>,表示如果模型改变,则删除并重建数据库,Database的CompatibleWithModel方法可用于检查模型是否发生改变;DropCreateDatabaseAlways<TContext>,表示每次使用DbContext都重建数据库;CreateDatabaseIfNotExists<TContext>,表示如果数据库不存在则创建数据库。用户选择使用策略时最好继承者三个类中的一个并重写Seed方法,该方法会在数据库创建后执行,此方法称为种子方法。

System.Data.Entity.Database类,可通过DbContext的Database属性获取Database类的实例,该实例可用于检查数据库的状态或执行一些数据库操作,如数据库是否存在,模型是否改变,创建或删除数据库操作,执行SQL脚本等操作。

对模型的定义可通过System.Component.DataAnnotations命名空间的特性,也可以通过Fluent API,Fluent API 的使用工具是DbModelBuilder类,此类可在DbContext的OnModelCreating方法的参数获得,通过Fluent API对模型映射所做的修改如同使用Data Annotation对模型所作的修改一样都会在数据迁移DbMigration中做出相应的反应,即数据迁移支持Fluent API和Data Annotation对模型的映射设置和修改。

 

Code First约定:

注:

主表(主体):具有主键的表。

从表(依赖实体):使用主表的主键作为本表的外键。

主键约定:类的“ID”名称的属性或“<类名>ID”的属性默认为模型的主键。

关系约定:模型的外键关系通过导航属性来推断。建议显示指定外键属性,外键的属性名称为“<导航属性名称>_<主体主键属性>”(如果外键属性是自动生成的,则导航属性名称与主体主键属性之间有下划线,如果是手写的可有可无)、“<主体类名><主体主键名称>”、“<主体模型名称>”。外键属性如果可为null,则外键关系可选的,且外键关系为非级联删除,如果不可为null则为必选的,并设置为级联删除。

对于无法推断出主键的引用类型的属性将作为复杂数据类型,在数据库中的体现是<复杂数据类型的属性名>_<复杂数据类型的类属性>。

当模型的导航属性无法在导航属性对应的另一个模型中找到合适的或应该的导航属性时,可使用InversePropertyAttribute手动指定导航属性对应的另一个模型中的导航属性。

当需要手动指定导航属性在模型中对应的外键属性时,使用ForeignKeyAttribute。

注意:

通过DbContext的访问类集合,返回的结果包括指定的类以及派生类,此结果在TPT策略下,通过执行外联接实现,在TPC下通过union操作实现,而正因此,导致在同时使用数据库的标识列和TPC策略下,派生类(可能多个)对应的表和基类对应的表中的记录不能具有相同的主键值,否则会导致DbContext内部调用的ObjectContext会创建重复的实体键,详情见TPC说明。

 

Fluent API的表映射策略:

每个继承层次结构一张表(TPH)(Fluent API的默认策略),每个类型一张表(TPT),每个具体类一张表(TPC);

单个实体对应多个表(实体拆分),将多个实体映射到一个表(表拆分)

TPH  Table per Hierarachy:每个层次结构一张表,表示继承此结构中的所有类型使用同一张表,这张表使用鉴别器列区分每行对应的模型,这个鉴别器列是对应鉴别器表“Discrimination”的外键,鉴别器表中具有鉴别器值,此值即为模型的类型名称。

modelBuilder.Entity<FatherClass>()

.Map<SonClass1>(m=>m.Requires(“Type”).HasValue(“SonClass1”))

.Map<SonClass2>(m=>m.Requires(“Type”).HasValue(“SonClass2”))

如果要求多态关系或查询,并且子类声明了很少的属性甚至子类的不同在于行为的话,建议使用TPH。目标是尽可能的减少列,并且在长期需求中这种非标准的结构不会引发问题。

TPH策略中,基础类型的字段会被用设为可为空,这会导致不可为空的外键属性因成为可为空而引发问题。

 

TPT  Table per Type:每个类型一张表,表示继承结构中的基类和派生类分别对应一张表,表中只映射对应类中定义的属性,不映射继承的属性,派生类对应的表和基类对应的表通过外键进行关联,以表示模型的继承的关系。

modelBuilder.Entity<SonClass1>().ToTable(“SonClass1”);

modelBuilder.Entity<SonClass2>().ToTable(“SonClass2”);

如果要求多态关系或查询,并且子类定义了较多的属性的话,建议使用TPT,或者如果在继承结构较复杂的情况下,join查询比union查询较节省资源的话,使用TPT,反之使用TPC。

 

TPC  Table per Concrete Type:每个具体类一张表,表示继承层级中的基类和派生类各自对应一张表,基类对应的表与派生类对应的表没有外键关系,派生类的属性包括继承的属性均映射到数据表中。使用MapInheritedProperties方法指示将继承的属性映射到数据表。此策略与数据库的标识列存在矛盾的情况,这个表映射策略会因为多个派生类以及基类具有同名称的标识列类型的主键属性,导致DbContext内部使用的ObjectContext的跟踪机制创建了重复的实体键(EntityKey)从而导致了数据插入失败,可以关闭标识列或设置为其他类型主键来手动设置主键属性来规避这个异常。

modelBuilder.Entity<FatherClass>()

.Property(c => c.ID)

.HasDatabaeGeneratedOption(DatabaseGeneratedOption.None)//在同时保存父类和子类时虽数据表没有外键关联,但因为继承关系会引发主键相关的异常,修改为手动设置主键则不会有此问题。

modelBuilder.Entity<SonClass1>().Map(m => {

m.MapInheritedProperties();

m.ToTable(“SonClass1”);

})

modelBuilder.Entity<SonClass2>().Map(m=>{

m.MapInheritedProperties();

m.ToTable(“SonClass2”);

})

如果不要求多态关系或查询,建议使用TPC,当很少查询基类或者很少有其他类类与基类关联,并且基类很少改动的情况下建议对继承树的高层使用TPC。

注意:使用TPC的模型所继承的属性不能来自这样的基类,这个基类进行了实体拆分,或所处的表进行了表拆分。

 

实体拆分:将单个实体映射到多个表,通过多次调用Map方法,将不同的属性分配到不同的表中。

modelBulder.Entity<Department>()

.Map(m=>{

m.Properties(t=>new{t.DepartmentID,t.Name});

m.ToTable(“Department”);’

})

.Map(m=>{

m.Properties(t=>new{t.DepartmentID,t.Administrator,t.StartDate,t.Budget});

m.ToTable(“DepartmentDetails”);

});

表拆分:将多个实体映射到一个表,通过指定相同的主键将两个实体映射到同一个表

modelBuilder.Entity<OfficeAssignment>().HasKey(t => t.InstructorID);

modelBuilder.Entity<Instructor>().HasRequired(t=>t.OfficeAssignment).WithRequriedPrincipal(t=>t.Instructor);

modelBuilder.Entity<OfficeAssignment>().ToTable(“Instructor”);

modelBuilder.Entity<Instructor>().ToTable(“Instructor”);

 

表关系映射与导航属性:

使用HasMany、HasRequired、HasOptional方法设置设置模型的导航属性的类型为多、单个必须、单个可选,这些方法返回的OptionalNavigationPropertyConfiguration类中With开头的方法用于设置关系类中导航属性和主体与依赖对象的关系。

略:普通的一对一与一对多关系示例代码

多对多关系:

modelBuilder.Entity<Model1>().HasMany(t1=>t1.NaviPropertyCollection).WithMany(t2=>t2.NaviPropertiyCollection)

多对多关系的表之间会自动增加一个关系表。

modelBuilder.Entity<Model1>()

.HasMany(m1=>m1.Model2Collection)

.WithMany(m2=>m2.Model1Collection)

.Map(m=>{

m.ToTable(“Many1ToMany2”)//指定关系表的表名

m.MapLeftKey(“Many1ID”);//指定关系表中对应设置表的外键列名

m.MapRightKey(“Many2ID”);//指定关系表中对应设置表相关联的关系表的外键列名

});

 

级联删除:

如果外键不可为空,则默认级联删除,否则,不是级联删除,主体删除后,外键置为null

modelBuilder.Convertions.Remove<OneToManyCascadeDeleteConvertion>();//移除当一对多必选关系时默认的级联删除选项

modelBuilder.Convertions.Remove<ManyToManyCascadeDeleteConvertion>();//移除当多对多必选关系时默认的级联删除

使用WillCascadeOnDelete方法手动设置是否级联删除

modelBuilder.Entity<TModel>()

.HasRquired(t=>t.NaviProperty)

.WithMany(anotherT=>anotherT.NaviPropertyCollection)

.HasForeighKey(t=>t.ForeighId)

.WillCascadeOnDelete(false)//设置为不级联删除

 

DbContext编写:

添加公共的DbSet类型或IDbSet类型的属性会由DbContext默认调用设置DbSet实例,如:

public IDbSet<TModel> TModels{get;set;}

public DbSet<TModel> TModels{get;set;}

访问实例也可通过Set方法,下面两行代码等效:

DbSet<TModel> result = dbContext.TModels;

DbSet<TModel> result = dbContext.Set<TModel>();

在访问DbSet属性但又仅仅进行只读操作时,为了提高性能可禁用跟踪,通过DbSet实例的AsNoTracking方法禁用追踪。

 

实体关系的修改:

实体关系可通过外键属性或导航属性进行修改,但只有执行DbContext的SaveChange后,外键属性以及导航属性才会反应真是的情况,如,将实体的导航属性进行了设置,这是对应的外键属性(如果有的话)可能为0(或其他表示未指定的值),只有执行SaveChange方法后,导航属性对应的外键值才为关联的模型的主键值。外键属性与导航属性的同步会在DetectChanged方法来完成,以下方法会自动调用DetectChanges方法:

DbSet.Add,DbSet.Find,DbSet.Remove,DbSet.Local,DbContext.SaveChange,DbContext.Attach,DbContext.GetValidationErrors,DbContext.Entry,DbChagne.Tracher.Entries,DbSet执行的linq查询。

删除关系可通过将导航属性设置为null的方式进行,亦或执行如下代码:

dbContext.Entry(tModel).Reference(c=>c.NaviProperty).CurrentValue=null;

通过ObjectContext的ObjectStateManager.ChangeRelationshipState方法也可修改关系:

((IObjectContextAdapter)dbContext).ObjectContext.ObjectStateManager.ChangeRelationshipState(model,anotherModel,m=>m.AnotherModel,EntityState.Added);//EntityState.Deleted表示删除,如果是更新关系的话需要Entity.Added新关系并Deleted旧关系。

DbSet.Local属性说明:该属性包含所有上下文中的对象,包括那些Add的但未保存到数据库的对象;但不包括Remove的但仍在数据库中的对象。

导航属性数据的加载:

导航属性对应的数据默认是惰性加载(在访问时才加载),如果希望查询数据时一同加载关联属性,须调用DbSet的Include方法并指定要求预加载的导航属性数据。

Var result = dbContext.Models.Include(b=>b.NaviProperty);//单级加载

Var result = dbContext.Models.Include(“NaviProperty.SecondNaviProperty”);//多级加载,加载Model的导航属性NaviProperty以及导航属性的导航属性SecondNaviProperty。

导航属性的惰性加载在首次访问时自动进行加载,加载通过继承自模型类的代理类进行,该代理类会重写导航属性,因此,模型类的导航属性必须用virtual修饰,不适用virtual修饰意味着惰性加载的关闭,在DbContext的构造方法中调用Configuration属性(DbContextConfiguration实例)的LazyLoadingEnabled=false也可设置惰性加载为关闭。

显示加载通过DbReferenceEntry的Load方法执行:

dbContext.Entry(model).Reference(m=>m.NavProperty).Load();

在更改外键属性后,可通过Load的方法显示加载同步的导航属性数据。通过dbContext的Entry访问代表集合类型的导航属性的DbCollectionEntry的Query方法获取可操作的IQueryable,来实现对部分实体的导航数据进行加载的要求。

代理类:

在为POCO实体类型实例化时,实体框架会动态生成派生自POCO实体的类,成为代理类,这些代理类通过重写POCO实体的virtual成员来实现一些自动操作,如之前提到的惰性加载。DbContext的Configuration属性(DbContextConfiguration实例)的ProxyCreationEnabled=false可将关闭代理类的生成。

实体的管理:

添加实体可通过DbSet的Add方法,也可通过dbContext的Attach方法并更改返回的Entry的状态为Added的方式。

如果仅仅希望保存实体的某个字段的修改到数据库的话,可通过Entry方法获取的DbEntityEntry实例并通过Property方法获取DbPropertyEntry对象实例并修改IsModified属性为true,这样就可以在保存数据库时,仅仅将IsModified=true的属性保存到数据库。

在调用SaveChange时,可能会引发DbUpdateConcurrentException,这表示更新时的并发异常,可通过dbContext.Entry(model)获取修改实体的DbEntityEntry实例,修改OriginValue和CurrentValue的方式来尝试消除引发并发的数据,DbEntityEntry的GetDatabaseValue方法可用来获取当前数据库中的数据,在重写原始值(OriginValue)或当前值(CurrentValue)时可能会使用到。

 

ASP.NET MVC 中的HttpContextWrapper怎么获取POST请求的所有参数内容

mikel阅读(496)

ASP.NET MVC中,可以通过HttpContextWrapper获取POST请求的所有参数内容。以下是具体方法和步骤:

1. 获取表单数据(Form Data)

适用于application/x-www-form-urlencodedmultipart/form-data类型的POST请求:

csharp

// 创建HttpContextWrapper实例(通常在控制器中可直接使用Request属性)
var httpContextWrapper = new HttpContextWrapper(System.Web.HttpContext.Current);

// 获取POST表单数据
NameValueCollection formData = httpContextWrapper.Request.Form;

// 遍历所有参数
foreach (string key in formData.AllKeys)
{
    string value = formData[key];
    // 处理键值对(例如:key="username", value="John")
}

2. 获取JSON/RAW请求体

适用于application/json或其他原始数据类型的POST请求:

csharp

using (var reader = new StreamReader(httpContextWrapper.Request.InputStream))
{
    // 重置流位置(重要!)
    httpContextWrapper.Request.InputStream.Seek(0, SeekOrigin.Begin);
    
    // 读取原始内容
    string rawBody = reader.ReadToEnd();
    
    // 示例:解析JSON
    // dynamic jsonData = JsonConvert.DeserializeObject(rawBody);
}

3. 获取查询字符串参数(Query String)

如果POST请求同时包含URL查询参数:

csharp

NameValueCollection queryParams = httpContextWrapper.Request.QueryString;
// 遍历方式同Form

4. 统一获取所有参数(Form + QueryString)

csharp

NameValueCollection allParams = httpContextWrapper.Request.Params; // 包含Form和QueryString

关键注意事项:

  1. 流的位置重置
    读取InputStream前必须重置流位置,因为ASP.NET默认不会重置流:csharphttpContextWrapper.Request.InputStream.Seek(0, SeekOrigin.Begin);
  2. 内容类型检查
    根据Content-Type选择处理方式:csharpif (httpContextWrapper.Request.ContentType.Contains(“application/json”)) { // 处理JSON } else if (httpContextWrapper.Request.ContentType.Contains(“application/x-www-form-urlencoded”)) { // 处理表单 }
  3. 在控制器中的简化写法
    在Controller中可直接用基类属性(无需手动创建Wrapper):csharppublic ActionResult YourAction() { var formData = Request.Form; // 表单数据 var jsonData = new StreamReader(Request.InputStream).ReadToEnd(); // 原始Body return View(); }

完整示例(获取所有POST参数并转为字典):

csharp

        /// <summary>
/// 获取所有请求参数
/// </summary>
/// <param name="context">http内容</param>
/// <returns></returns>
public Dictionary<string, string> GetAllPostParameters(HttpContextBase context)
{
var parameters = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

// 处理表单数据
foreach (string key in context.Request.Form.AllKeys)
{
parameters[key] = context.Request.Form[key];
}

// 处理 JSON 数据
if (context.Request.ContentType?.StartsWith("application/json", StringComparison.OrdinalIgnoreCase) == true)
{
string json = new StreamReader(context.Request.InputStream, Encoding.UTF8).ReadToEnd();
try
{
// 重置并读取流
context.Request.InputStream.Seek(0, SeekOrigin.Begin);


if (string.IsNullOrWhiteSpace(json))
{
// 记录空 JSON 警告
log.Debug("收到空的 JSON 请求体");
return parameters;
}

// 尝试解析 JSON
var jsonData = JObject.Parse(json);

foreach (var prop in jsonData.Properties())
{
parameters[prop.Name] = prop.Value?.ToString() ?? string.Empty;
}
}
catch (JsonReaderException jex)
{
// 处理 JSON 解析错误
log.Error($"JSON 解析错误: {jex.Message}");

// 尝试解析为查询字符串格式
try
{
var keyValues = HttpUtility.ParseQueryString(json);
foreach (string key in keyValues.AllKeys)
{
parameters[key] = keyValues[key];
}
}
catch
{
// 如果两种方式都失败,保存原始内容
parameters["__raw_body"] = json;
}
}
catch (Exception ex)
{
log.Error($"处理 JSON 请求时出错: {ex.Message}");
}
}

return parameters;
}

// 使用示例
var wrapper = new HttpContextWrapper(HttpContext.Current);
var allParams = GetAllPostParameters(wrapper);

推荐做法:

  • 优先使用模型绑定ASP.NET MVC的模型绑定机制更安全高效。csharp[HttpPost] public ActionResult Submit(YourViewModel model) // 自动绑定POST数据 { // 直接使用model.Property }
  • 直接操作Request对象仅适用于动态数据或特殊情况。

MySQL误删数据了,如何快速恢复? - 苏三说技术 - 博客园

mikel阅读(239)

来源: MySQL误删数据了,如何快速恢复? – 苏三说技术 – 博客园

前言

最近星球中有位小伙伴说:他不小心把测试环境MySQL表中所有数据都误删了,问我要如何快速恢复?

幸好他误删的是测试环境,非生产环境。

我遇到过,之前有同事把生产环境会员表中的数据误删除的情况。

这篇文章跟大家一起聊聊MySQL如果误删数据了,要如何快速恢复。

希望对你会有所帮助。

1.为什么数据恢复如此重要?

2023年某电商平台误删20万用户数据,导致直接损失800万

某金融机构DBA误执行DROP TABLE,系统停摆6小时

这些事故背后,暴露的是误删数据之后恢复方案的缺失。

数据丢失的三大元凶

  1. 人为误操作(占75%):DELETE忘加WHERE、DROP TABLE手滑
  2. 程序BUG(占20%):循环逻辑错误、事务未回滚
  3. 硬件故障(占5%):磁盘损坏、机房断电

下面是数据丢失的主要原因:
image

那么,如果MySQL如果误删数据了,快速恢复数据的方案有哪些呢?

2.常见的数据恢复方案

方案1:Binlog日志恢复

该方案最常用。

适用场景:误执行DELETE、UPDATE

恢复流程

image

操作步骤

  1. 定位误操作位置
mysqlbinlog --start-datetime="2023-08-01 14:00:00" \
           --stop-datetime="2023-08-01 14:05:00" \
           mysql-bin.000001 > /tmp/err.sql
  1. 提取回滚SQL(使用python工具)
# parse_binlog.py
import pymysql
from pymysqlreplication import BinLogStreamReader

stream = BinLogStreamReader(
   connection_settings = {
       "host": "127.0.0.1",
       "port": 3306,
       "user": "root",
       "passwd": "root"},
   server_id=100,
   blocking=True,
   resume_stream=True,
   only_events=[DeleteRowsEvent, UpdateRowsEvent])

for binlogevent in stream:
   for row in binlogevent.rows:
       if isinstance(binlogevent, DeleteRowsEvent):
           # 生成INSERT语句
           print(f"INSERT INTO {binlogevent.table} VALUES {row['values']}")
       elif isinstance(binlogevent, UpdateRowsEvent):
           # 生成反向UPDATE
           print(f"UPDATE {binlogevent.table} SET {row['before_values']} WHERE {row['after_values']}")
  1. 执行恢复
python parse_binlog.py | mysql -u root -p db_name

方案2:延迟复制从库

该方案是金融级的方案。

适用场景:大规模误删数据

架构原理
image

配置步骤

  1. 设置延迟复制
STOP SLAVE;
CHANGE MASTER TO MASTER_DELAY = 1800; -- 延迟30分钟(1800秒)
START SLAVE;
  1. 误删后立即停止同步
STOP SLAVE;
  1. 将延迟从库提升为主库
RESET SLAVE ALL;
SHOW MASTER STATUS; -- 记录binlog位置

方案3:全量备份+增量恢复

适用场景:整表或整库误删

恢复流程

image

操作步骤

  1. 恢复全量备份
mysql -u root -p db_name < full_backup_20230801.sql
  1. 应用增量日志(跳过误操作点)
mysqlbinlog --start-position=100 --stop-position=500 \
          mysql-bin.000001 | mysql -u root -p

方案4:Undo日志恢复

该方案是InnoDB特有的。

适用场景:刚提交的误操作(事务未关闭)

核心原理
image

操作步骤

  1. 查询事务信息
SELECT * FROM information_schema.INNODB_TRX;
  1. 定位Undo页
SHOW ENGINE INNODB STATUS;
  1. 使用undrop-for-innodb工具
./undrop-for-innodb/system_parser -t user_data /var/lib/mysql/ibdata1

方案5:文件恢复

从物理备份中恢复,需要提前做备份。

适用场景:DROP TABLE误操作

恢复流程

image

操作步骤

  1. 安装恢复工具
yum install testdisk -y
  1. 扫描磁盘
photorec /dev/sdb1
  1. 重建表结构
CREATE TABLE user_data (...) ENGINE=InnoDB;
  1. 导入表空间
ALTER TABLE user_data DISCARD TABLESPACE;
cp recovered.ibd /var/lib/mysql/db_name/user_data.ibd
ALTER TABLE user_data IMPORT TABLESPACE;

方案6:云数据库快照恢复

适用场景:阿里云RDS、AWS RDS等云服务

操作流程(以阿里云为例)

image

最佳实践

  1. 设置策略:
    • 保留7天快照
    • 每4小时增量备份
  2. 误删后操作:
# 通过SDK创建临时实例
aliyun rds CloneInstance --DBInstanceId rm-xxxx \
                       --BackupId 111111111 \
                       --PayType Postpaid

3、恢复方案对比选型

方案 恢复粒度 时间窗口 复杂度 适用场景
Binlog日志恢复 行级 分钟级 小范围误删
延迟复制从库 库级 小时级 核心业务数据
全量+增量恢复 库级 小时级 整库丢失
Undo日志恢复 行级 秒级 极高 事务未提交
文件恢复 表级 不确定 极高 DROP TABLE操作
云数据库快照 实例级 分钟级 云环境

4.如何预防误删数据的情况?

4.1 权限控制(事前预防)

核心原则:最小权限分配

-- 禁止开发直接操作生产库
REVOKE ALL PRIVILEGES ON *.* FROM 'dev_user'@'%';

-- 只读账号配置
GRANT SELECT ON app_db.* TO 'read_user'@'%';

-- DML权限分离
CREATE ROLE dml_role;
GRANT INSERT, UPDATE, DELETE ON app_db.* TO dml_role;

4.2 操作规范(事中拦截)

  1. SQL审核:所有DDL必须走工单
  2. 高危操作确认:执行DROP前二次确认
-- 危险操作示例
DROP TABLE IF EXISTS user_data; -- 必须添加IF EXISTS
  1. WHERE条件检查:DELETE前先SELECT验证

4.3 备份策略(事后保障)

黄金备份法则:321原则

  • 3份备份(本地+异地+离线)
  • 2种介质(SSD+磁带)
  • 1份离线存储

总结

下面给大家总了数据恢复的三要三不要。

三要

  1. 立即冻结现场:发现误删马上锁定数据库。
  2. 优先使用Binlog:90%场景可通过日志恢复。
  3. 定期演练恢复:每季度做恢复测试。

三不要

  1. 不要心存侥幸:认为误删不会发生在自己身上。
  2. 不要盲目操作:恢复前先备份当前状态。
  3. 不要忽视监控:设置删除操作实时告警。

设计系统时,永远假设明天就会发生数据误删。

当灾难真正降临时,你会发现所有的预防措施都是值得的。

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

本文收录于我的技术网站http://www.susan.net.cn