三层开发中容易犯的错误


前记:

相信大家对三层开发都已经耳熟能详,可是我却发现新公司的既有代码中有一些违背分层开发思想的东西,现在与大家分享这些错误,我们共勉之。

如果有人觉得对三层开发拿捏得不是太准,请参照李天平的文章:分层开发思想与小笼包,这篇文章用隐喻说明分层开发,是非常好的一篇文章。

正文:

1.界面层参与非界面逻辑,抢业务逻辑层的饭碗

什么是界面逻辑:

界面层应该有的逻辑就是显示的逻辑,例如根据逻辑结果显示某一个Panel不显示另外一个Panel,或者有一个数据集应该在界面上怎么呈现,这是界面层的逻辑

例子场景:

用户登录时首先验证用户输入的用户名是否有效,如果用户名有效,然后再验证用户输入的密码是否和用户名匹配,如果匹配则表示用户可以登录,增加用户的登录次数,然后将用户的信息写入Session中;否则返回错误。在这个过程中除了将用户信息写入Session这一步属于界面逻辑以外其他的操作都应该放在业务逻辑层。

错误代码示例:

private void buttonLogin_Click(object sender, EventArgs ev)
        {
            string userName = textBoxUserName.Text;
            string password = textPassword.Text;

            if (Business.Account.Exists(userName))
            {
                bool success = Business.Account.Login(userName, password);
                if (success)
                {
                    Business.Account.AddLoginTime(userName);

                    Session["user"] = new User(userName, password);
                    Redirect("/");
                }
                else
                {
                    this.labelMessage.Text = "登录失败。";
                }
            }
            else
            {
                this.lableMessage.Text = "用户名不存在。";
            }
        }

 

分析:在上面的代码中一个UI层的一个事件中调用了三次rules层的方法:

Business.Account.Exists(userName)

Business.Account.Login(userName, password)

Business.Account.AddLoginTime(userName);

还附加有条件判断,这种方法在执行效果上面是没有什么错误的,可是却造成了逻辑前移;本来应该在逻辑层执行的判断放在了界面层,是不合适的。

2.数据访问层参与了大量的业务逻辑

这种现象经常出现在大量使用存储过程的系统中,将一大堆逻辑统统放在一个存储过程中实现了,乍一看可能很有效率,其实造成了系统结构的失调,给维护带来困难,数据访问层甚者数据库要抢逻辑层的饭碗了。

还以用户登录为例:

下面是业务逻辑层的登录方法: 

//业务逻辑层的登录方法
        public int Login(string userName, string password) 
        {
            return DataAccess.UserAccount.Login(userName, password);
        }


 

下面是数据层的登录方法: 

//数据访问层的登录方法
        public int Login(string userName, string password)
        { 
            SqlParameter[] parameters = new SqlParameter[]{
                // 
            };
            return SqlHelper.ExecuteProcedure("Login",parameters);
        }


 

下面是登录的存储过程: 

CREATE PROC Login
            @userName varchar(20),
            @password varchar(20)
         AS 
            IF NOT EXISTS(SELECT * FROM UserAccount WHERE UserName = @userName)
                RETURN -1
            IF NOT EXISTS(SELECT * FROM UserAccount WHERE UserName = @userName AND password = @password)
                RETURN 1
         
            UPDATE UserAccount
            SET LoginTimes = LoginTimes + 1
            WHERE UserName = @userName
         
            RETURN 0


 

分析:从上面三段代码中我们可以很显然得看到登录的业务逻辑已经全部被后移到了数据库的存储过程中。这样使用的三层结构就失去了意义,逻辑层名存实亡了;而数据库的压力会越来越大;我们修改业务逻辑的时候不是到逻辑层修改,而是要到数据库中去修改了。

3.将界面层上的数据组件(如SqlDataSource)作为参数传递到业务逻辑层去赋值

这样做的坏处很明显,本来是界面层依赖于业务逻辑层的,现在业务逻辑层反过来去依赖界面层的类,需要逻辑层引用System.Web命名空间,显然是错误的。

4.为了省事儿,不是直接将参数传递到业务逻辑层,而是通过HttpContext直接获得界面层应该传递的参数

例子:在系统设计的初期没有记录用户登录的IP地址,而到了后期发现了这个问题,要求纪录用户IP了,为了不修改业务逻辑层方法的定义,也不用修改界面层的调用方法,于是便写出了下面的代码:

public int Login(string userName, string password)
        {
            string userIP = System.Web.HttpContext.Current.Request.UserHostAddress;
            //follow is login steps
        }

 

这一点犯的错误和3中的错误相同,导致层之间形成了依赖环。

5.将事务处理放在数据访问层来做

事务处理应该放在业务逻辑层处理,原因是

1)事务的划分是根据业务逻辑来定义的,通常一个事务就代表完成了一个完整的逻辑操作;

2)一个业务逻辑可能有几个数据操作,修改几个表中的数据,而通常数据层的类的划分是根据数据库表来划分的,如果把事务处理放在数据访问层,那么业务层的方法需要调用两个以上的数据层方法时,就会出现执行两个事务的情况,显然这是不合理的。

总结:

1.在三层结构的划分中我们应该使三层各负其责,谁也不能越权,谁也不能懒惰,通常情况下,逻辑层应该在满足各负其责的条件下,尽可能的厚。

2.三层结构中的依赖关系很明确,界面层依赖于逻辑层,逻辑层依赖于数据访问层,不能互相依赖,而形成依赖环。

转自:

http://www.cnblogs.com/yukaizhao/archive/2007/03/08/layer_develop_errors.html



<think>嗯,用户现在想了解在SpringBoot开发中使用MySQL锁的具体场景最佳实践。这个问题很实际,因为在企业级应用中,并发控制直接关系到数据一致性系统稳定性。用户之前已经探讨过MySQL锁机制与隔离级别的关系,说明ta对数据库底层原理有一定了解。现在ta更关注的是在SpringBoot框架下的工程化实现,这是理论到实践的跨越。从ta引用的资料来看,ta正在整合SpringBoot、MySQLDruid,可能正在开发一个需要高并发处理的系统。考虑从两个维度展开回答:业务场景维度(为什么需要锁)技术实现维度(怎么用锁)。业务场景方面,需要重点分析电商库存、金融交易这些对数据一致性要求极高的场景。技术实现上,要区分悲观锁乐观锁的实现差异。注意到用户引用了MyBatis缓存配置,所以在技术实现部分要特别说明在MyBatis的Mapper中如何写锁语句,同提醒缓存与数据库一致性的问题。用户似乎在使用SpringBoot2/3,要注意不同版本的事务管理差异。在最佳实践部分,应该强调锁的粒度控制。用户可能容易错误是过度使用表锁,这在高并发场景是致命的。需要给出具体的行锁示例代码,特别是Spring声明式事务注解的正确用法。上次讨论过可重复读隔离级别与间隙锁的关系,这里可以结合具体场景说明何依赖数据库机制,何需要应用层补充锁。风险控制部分必须包含死锁案例,这是生产环境最容易出现的问题。用户可能没意识到乐观锁的ABA问题,需要通过version字段的原子性更新来解决。扩展问题部分应该引导用户思考分布式锁场景,因为当ta的系统扩展为集群,MySQL行锁就不再够用了。</think> 在 Spring Boot 开发中合理使用 MySQL 锁是保障高并发场景下数据一致性的关键手段。以下是具体场景实践建议: ### 一、典型使用场景 1. **库存扣减** - **问题**:超卖问题(库存减为负) - **解决方案**: ```sql UPDATE products SET stock = stock - 1 WHERE id = 1001 AND stock > 0 /* 行锁自动生效 */ ``` - **技术细节**:WHERE 条件+行锁确保原子操作[^1] 2. **金融交易(转账)** - **需求**:双账户余额同步更新 - **代码示例**: ```java @Transactional(isolation = Isolation.REPEATABLE_READ) public void transfer(Long fromId, Long toId, BigDecimal amount) { // 显式添加行锁 Account from = accountMapper.selectForUpdate(fromId); Account to = accountMapper.selectForUpdate(toId); if (from.getBalance().compareTo(amount) >= 0) { accountMapper.updateBalance(fromId, amount.negate()); accountMapper.updateBalance(toId, amount); } } ``` Mapper 中的锁语句: ```xml <select id="selectForUpdate" resultType="Account"> SELECT * FROM account WHERE id = #{id} FOR UPDATE </select> ``` 3. **唯一性校验** - **场景**:注册用户名防重复 - **实现**: ```sql SELECT * FROM users WHERE username = 'test' FOR UPDATE /* 间隙锁防插入 */ ``` 后执行插入操作,可重复读隔离级别下间隙锁阻止幻读 ### 二、最佳实践 1. **锁粒度控制** - 优先使用行锁而非表锁 - 确保 WHERE 条件命中索引(否则退化为表锁) - 示例:`WHERE order_id = 2001`(order_id 需建索引) 2. **事务设计原则** - 事务范围最小化 ```java // 错误示范:方法内包含远程调用 @Transactional public void createOrder(Order order) { orderMapper.insert(order); inventoryService.reduceStock(); // 远程调用 paymentService.pay(); // 事务间过长 } ``` 3. **混合锁策略** | 场景 | 推荐方案 | 优势 | |---------------------|------------------------|--------------------------| | 读多写少 | 乐观锁(@Version) | 无锁冲突,高并发读 | | 写密集 | 悲观锁(FOR UPDATE) | 强一致性保障 | | 分布式环境 | Redis分布式锁 | 跨进程控制 | 4. **隔离级别选择** - 默认 `REPEATABLE_READ`:平衡性能与一致性(启用MVCC+间隙锁) - 秒杀场景可降级为 `READ_COMMITTED`:减少锁等待(需业务层补偿) ### 三、风险规避 1. **死锁预防** - 统一资源获取顺序(如先锁A后锁B) - 设置事务超:`@Transactional(timeout=3)` - 监控工具:`SHOW ENGINE INNODB STATUS` 2. **连接池配置要点** ```properties # Druid配置 spring.datasource.druid.max-active=20 spring.datasource.druid.max-wait=1000 # 获取连接超间(ms) ``` 避免连接耗尽导致锁等待超 3. **锁超设置** ```sql SET innodb_lock_wait_timeout=3; /* 会话级锁等待超 */ ``` ### 四、性能优化 1. **异步扣减库存** ```java @Async public void asyncReduceStock(Long productId) { // 使用最终一致性方案 redisTemplate.opsForValue().decrement("stock:"+productId); } ``` 结合 Redis 预减库存,MySQL 异步持久化 2. **批量操作加锁优化** ```sql SELECT * FROM orders WHERE user_id=100 AND status='UNPAID' FOR UPDATE SKIP LOCKED /* 跳过已锁定行 */ ``` > **关键决策树**: > ```mermaid > graph TD > A[需要强一致性?] --> |是| B[写冲突频率?] > A --> |否| C[使用无锁方案] > B --> |高| D[选择悲观锁FOR UPDATE] > B --> |低| E[选择乐观锁version] > ``` --- **相关问题**: 1. Spring Boot事务失效的常见场景有哪些? 2. 如何在分库分表架构中实现分布式锁? 3. MySQL死锁的产生条件与排查方法是什么? 4. Redis与MySQL如何协同保障数据最终一致性? 5. 高并发场景下如何选择数据库隔离级别优化性能?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值