悲观锁的应用场景
悲观锁的基本思想是假设并发冲突会发生,因此在操作数据时会先锁定数据,直到完成操作并提交事务后才释放锁。这种方式适用于写操作较多、并发冲突可能性较高的场景。
高写入比例的数据库操作:如果系统中有很多写操作,并且这些写操作可能会频繁地相互干扰,那么使用悲观锁可以有效避免数据不一致的问题。
对数据一致性要求高的场景:比如金融交易系统,银行转账,高并发点赞等,需要确保在任何时刻的数据都是一致的,不允许出现脏读、不可重复读等问题。
乐观锁的应用场景
乐观锁则假定并发冲突不会经常发生,因此它不会在开始操作时就锁定资源,而是在提交更新时检查是否有其他事务已经修改了该数据。如果检测到冲突,则拒绝此次操作。乐观锁更适用于读多写少的环境。
读操作远多于写操作的场景:例如在线阅读平台、新闻网站等,这类应用主要以读取信息为主,很少会有数据修改的需求,采用乐观锁可以减少锁带来的性能损耗。
低冲突概率的环境:当系统预期不同事务之间很少会对同一数据进行修改时,使用乐观锁可以获得更好的性能表现。比如库存管理系统中,对于非热销商品的库存调整。
选择哪种锁机制应基于具体的应用场景以及系统对性能和一致性的需求来决定。在实际开发过程中,还需要考虑死锁预防、锁的粒度等因素。
点赞悲观锁实现
以下是基于 Java 和 MyBatis-Plus 实现悲观锁的点赞功能的完整代码示例。
1. 数据库表设计
假设我们有一个 articles 表,包含以下字段:
id: 文章唯一标识。
title: 文章标题。
content: 文章内容。
likes: 点赞数。
sql
深色版本
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
likes INT DEFAULT 0
);
2. 配置 MyBatis-Plus 和数据库连接
2.1 添加依赖
在 pom.xml 文件中添加 MyBatis-Plus 和 MySQL 驱动依赖(与之前一致)。
2.2 配置数据库连接
在 application.yml 中配置数据库连接信息(与之前一致)。
3. 实体类和 Mapper 接口
3.1 创建实体类
java
深色版本
java
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("articles")
public class Article {
@TableId
private Long id;
private String title;
private String content;
private Integer likes;
}
3.2 创建 Mapper 接口
java
深色版本
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
/**
* 悲观锁查询文章
*/
@Select("SELECT * FROM articles WHERE id = #{id} FOR UPDATE")
Article selectForUpdate(Long id);
/**
* 更新点赞数
*/
@Update("UPDATE articles SET likes = likes + 1 WHERE id = #{id}")
int incrementLikes(Long id);
}
4. 实现点赞功能
4.1 Service 层实现
悲观锁的核心在于使用 FOR UPDATE 锁定数据行,确保在事务提交前其他事务无法修改该行数据。
java
深色版本
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
/**
* 使用悲观锁实现点赞
*/
@Transactional // 开启事务
public String likeArticle(Long articleId) {
// 使用悲观锁查询文章(锁定该行)
Article article = articleMapper.selectForUpdate(articleId);
if (article == null) {
return "Article not found";
}
// 更新点赞数
int rows = articleMapper.incrementLikes(articleId);
if (rows > 0) {
return "Like successful, new likes: " + (article.getLikes() + 1);
} else {
return "Failed to update likes";
}
}
}
4.2 Controller 层实现
java
深色版本
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
/**
* 点赞接口
*/
@PostMapping("/{id}/like")
public String likeArticle(@PathVariable Long id) {
return articleService.likeArticle(id);
}
}
5. 测试接口
启动 Spring Boot 应用后,可以通过以下方式测试点赞功能:
请求示例
bash
深色版本
POST http://localhost:8080/articles/1/like
响应结果
成功时返回:"Like successful, new likes: X"
如果文章不存在,返回:"Article not found"
6. 核心原理
悲观锁的工作流程:
在查询文章时,使用 FOR UPDATE 锁定目标数据行。
在事务提交之前,其他事务无法对该行数据进行修改。
更新点赞数后提交事务,释放锁。
SQL 示例:
sql
深色版本
-- 查询并锁定数据行
SELECT * FROM articles WHERE id = 1 FOR UPDATE;
-- 更新点赞数
UPDATE articles SET likes = likes + 1 WHERE id = 1;
适用场景:
适用于写操作较多且并发冲突概率较高的场景。
例如,在高并发环境下对同一篇文章进行频繁点赞时,可以避免数据不一致的问题。
7. 注意事项
性能开销:
悲观锁会锁定数据行,可能导致其他事务阻塞,影响系统性能。
因此,只有在确实需要严格保证数据一致性时才使用悲观锁。
死锁风险:
如果多个事务同时尝试锁定不同的资源,可能会导致死锁。
需要合理设计事务逻辑,尽量减少死锁的可能性。
事务管理:
必须确保在事务中执行锁定和更新操作,否则锁定不起作用。
通过以上代码,我们可以利用悲观锁实现点赞功能,确保在高并发场景下的数据一致性。
点赞乐观锁实现
以下是使用 Java 和 MyBatis-Plus 实现乐观锁的代码示例。我们将基于 Spring Boot 和 MyBatis-Plus 框架完成点赞功能。
1. 数据库表设计
假设我们有一个 articles 表,包含以下字段:
id: 文章唯一标识。
title: 文章标题。
content: 文章内容。
likes: 点赞数。
version: 版本号(用于乐观锁)。
sql
深色版本
CREATE TABLE articles (
id INT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
likes INT DEFAULT 0,
version INT DEFAULT 0
);
2. 配置 MyBatis-Plus 和数据库连接
2.1 添加依赖
在 pom.xml 文件中添加 MyBatis-Plus 和 MySQL 驱动依赖:
xml
深色版本
org.springframework.boot
spring-boot-starter-web
<!-- MyBatis-Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL Driver -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Lombok (Optional for code simplification) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2.2 配置数据库连接 在 application.yml 中配置数据库连接信息:
yaml
深色版本
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: your_username
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
3. 实体类和 Mapper 接口
3.1 创建实体类
使用 @Version 注解来标识版本号字段。
java
深色版本
java
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.annotation.Version;
import lombok.Data;
@Data
@TableName("articles")
public class Article {
@TableId
private Long id;
private String title;
private String content;
private Integer likes;
@Version
private Integer version; // 版本号字段,用于乐观锁
}
3.2 创建 Mapper 接口
继承 MyBatis-Plus 提供的 BaseMapper 接口。
java
深色版本
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ArticleMapper extends BaseMapper<Article> {
}
4. 配置乐观锁插件
在 Spring Boot 启动类中配置 MyBatis-Plus 的乐观锁插件。
java
深色版本
java
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class MyBatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
}
5. 实现点赞功能
5.1 Service 层实现
编写点赞逻辑,使用乐观锁更新点赞数。
java
深色版本
java
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ArticleService {
@Autowired
private ArticleMapper articleMapper;
public String likeArticle(Long articleId) {
// 查询文章信息
Article article = articleMapper.selectById(articleId);
if (article == null) {
return "Article not found";
}
// 更新点赞数和版本号
article.setLikes(article.getLikes() + 1);
// 使用 MyBatis-Plus 自动处理乐观锁
int rows = articleMapper.updateById(article);
if (rows > 0) {
return "Like successful, new likes: " + article.getLikes();
} else {
return "Optimistic lock conflict, please try again";
}
}
}
5.2 Controller 层实现
提供 RESTful API 接口。
java
深色版本
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/articles")
public class ArticleController {
@Autowired
private ArticleService articleService;
@PostMapping("/{id}/like")
public String likeArticle(@PathVariable Long id) {
return articleService.likeArticle(id);
}
}
6. 测试接口
启动 Spring Boot 应用后,可以通过以下方式测试点赞功能:
请求示例
bash
深色版本
POST http://localhost:8080/articles/1/like
响应结果
成功时返回:"Like successful, new likes: X"
如果发生乐观锁冲突,返回:"Optimistic lock conflict, please try again"
7. 核心原理
乐观锁的核心思想:
在更新数据时,MyBatis-Plus 会自动检查 version 字段是否匹配。
如果匹配,则更新数据并递增 version。
如果不匹配,则更新失败,返回受影响行数为 0。
SQL 示例:
sql
深色版本
UPDATE articles
SET likes = likes + 1, version = version + 1
WHERE id = 1 AND version = current_version;
适用场景:
适用于读多写少且冲突概率较低的场景,如点赞、评论、浏览量统计等。
通过以上代码,我们可以轻松实现基于 MyBatis-Plus 的乐观锁功能,确保在高并发场景下的数据一致性。
银行转账悲观锁实现
在银行转账的场景中,悲观锁是一种非常合适的并发控制机制。由于银行转账涉及资金的流动,数据一致性至关重要,因此使用悲观锁可以确保在事务完成之前,相关账户不会被其他事务修改。
以下是基于 Java 和 MyBatis-Plus 实现银行转账功能的完整代码示例,其中使用了悲观锁来保证事务的安全性。
1. 数据库表设计
假设我们有一个 accounts 表,用于存储用户账户信息:
id: 账户唯一标识。
user_name: 用户名。
balance: 账户余额。
sql
深色版本
CREATE TABLE accounts (
id INT PRIMARY KEY AUTO_INCREMENT,
user_name VARCHAR(255) NOT NULL,
balance DECIMAL(10, 2) NOT NULL DEFAULT 0.00
);
2. 配置 MyBatis-Plus 和数据库连接
2.1 添加依赖
在 pom.xml 文件中添加 MyBatis-Plus 和 MySQL 驱动依赖(与之前一致)。
2.2 配置数据库连接
在 application.yml 中配置数据库连接信息:
yaml
深色版本
spring:
datasource:
url: jdbc:mysql://localhost:3306/your_database?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
username: your_username
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
3. 实体类和 Mapper 接口
3.1 创建实体类
java
深色版本
java
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.math.BigDecimal;
@Data
@TableName("accounts")
public class Account {
@TableId
private Long id;
private String userName;
private BigDecimal balance;
}
3.2 创建 Mapper 接口
java
深色版本
java
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
@Mapper
public interface AccountMapper extends BaseMapper<Account> {
/**
* 悲观锁查询账户
*/
@Select("SELECT * FROM accounts WHERE id = #{id} FOR UPDATE")
Account selectForUpdate(Long id);
/**
* 更新账户余额
*/
@Update("UPDATE accounts SET balance = balance + #{amount} WHERE id = #{id}")
int updateBalance(Long id, BigDecimal amount);
}
4. 实现转账功能
4.1 Service 层实现
悲观锁的核心在于使用 FOR UPDATE 锁定账户行,确保在事务提交前其他事务无法修改账户余额。
java
深色版本
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
@Service
public class AccountService {
@Autowired
private AccountMapper accountMapper;
/**
* 使用悲观锁实现转账
*
* @param fromAccountId 转出账户 ID
* @param toAccountId 转入账户 ID
* @param amount 转账金额
*/
@Transactional // 开启事务
public String transfer(Long fromAccountId, Long toAccountId, BigDecimal amount) {
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return "Invalid transfer amount";
}
// 查询转出账户并加锁
Account fromAccount = accountMapper.selectForUpdate(fromAccountId);
if (fromAccount == null || fromAccount.getBalance().compareTo(amount) < 0) {
return "Insufficient balance or invalid account";
}
// 查询转入账户并加锁
Account toAccount = accountMapper.selectForUpdate(toAccountId);
if (toAccount == null) {
return "Invalid target account";
}
// 执行转账操作
accountMapper.updateBalance(fromAccountId, amount.negate()); // 减少转出账户余额
accountMapper.updateBalance(toAccountId, amount); // 增加转入账户余额
return "Transfer successful";
}
}
4.2 Controller 层实现
java
深色版本
java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
@RestController
@RequestMapping("/accounts")
public class AccountController {
@Autowired
private AccountService accountService;
/**
* 转账接口
*/
@PostMapping("/transfer")
public String transfer(
@RequestParam Long fromAccountId,
@RequestParam Long toAccountId,
@RequestParam BigDecimal amount) {
return accountService.transfer(fromAccountId, toAccountId, amount);
}
}
5. 测试接口
启动 Spring Boot 应用后,可以通过以下方式测试转账功能:
请求示例
bash
深色版本
POST http://localhost:8080/accounts/transfer?fromAccountId=1\&toAccountId=2\&amount=100.00
响应结果
成功时返回:"Transfer successful"
如果余额不足或账户无效,返回相应的错误信息。
6. 核心原理
悲观锁的工作流程:
在查询账户时,使用 FOR UPDATE 锁定目标账户的数据行。
在事务提交之前,其他事务无法对该账户进行修改。
完成转账操作后提交事务,释放锁。
SQL 示例:
sql
深色版本
-- 查询并锁定账户
SELECT * FROM accounts WHERE id = 1 FOR UPDATE;
-- 更新账户余额
UPDATE accounts SET balance = balance - 100.00 WHERE id = 1;
UPDATE accounts SET balance = balance + 100.00 WHERE id = 2;
适用场景:
适用于写操作较多且并发冲突概率较高的场景。
例如,在高并发环境下进行银行转账时,可以避免重复扣款或余额不一致的问题。
7. 注意事项
性能开销:
悲观锁会锁定数据行,可能导致其他事务阻塞,影响系统性能。
因此,只有在确实需要严格保证数据一致性时才使用悲观锁。
死锁风险:
如果多个事务同时尝试锁定不同的资源,可能会导致死锁。
需要合理设计事务逻辑,尽量减少死锁的可能性(例如,按照固定的顺序锁定账户)。
事务管理:
必须确保在事务中执行锁定和更新操作,否则锁定不起作用。
通过以上代码,我们可以利用悲观锁实现银行转账功能,确保在高并发场景下的数据一致性。