一、乐观锁和悲观锁
1、乐观锁
乐观锁是一种并发控制机制,它持有一种乐观的态度,相信数据冲突发生的概率较低,并允许多个任务并行地对数据进行操作,而不加锁。在乐观锁的机制下,对数据的操作不会立即进行冲突检测和加锁,而是在数据提交时通过一种机制来验证是否存在冲突。
乐观锁通常通过版本号(也称为时间戳)实现。每次读取数据时,都会获取当前版本号,并将其与修改前的版本号进行比对。如果两个版本号相同,则认为数据没有被其他任务修改,允许当前任务进行修改操作并更新版本号。如果版本号不同,则表示数据已被其他任务修改,此时需要处理冲突,通常是通过回滚操作或者给出适当的提示来解决冲突。
乐观锁适用于多读的应用类型,可以提高吞吐量。但需要注意的是,在写操作较多的场景下,乐观锁可能会因为版本不一致而不断重试更新,导致消耗大量CPU,影响性能。
2、悲观锁
悲观锁(Pessimistic Lock)是一种基于悲观态度的数据并发控制机制,用于防止数据冲突。它采取预防性的措施,在修改数据之前将其锁定,并在操作完成后释放锁定,以确保数据的一致性和完整性。
悲观锁的核心思想是,在数据被修改之前,假设会有其他使用者对其进行修改操作,因此采取主动的措施将数据加锁,以阻止其他人的修改操作。这种悲观的态度认为数据冲突是不可避免的,因此在修改数据之前先锁定数据,以防止冲突的发生。
在悲观锁的机制下,当一个使用者要修改某个数据时,首先会尝试获取该数据的锁。只有在当前使用者完成操作并释放锁之后,下一个使用者才能获取该数据的锁,继而对数据进行加锁和操作。因此,悲观锁具有强烈的独占和排他特性。
悲观锁的实现往往依靠数据库提供的锁机制,因为只有数据库层提供的锁机制才能真正保证数据访问的排他性。否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据。
需要注意的是,尽管悲观锁能够防止丢失更新和不可重复读这类并发问题,但它会影响并发性能,因为其他想要访问被锁数据的事务必须等待锁被释放。因此,应该谨慎地使用悲观锁,特别是在高并发的环境中。
传统的关系型数据库,如行锁、表锁、读锁、写锁等,都是悲观锁的具体实现形式。这些锁机制在操作数据之前先上锁,以确保数据的一致性和完整性。
3、模拟乐观锁
①场景
一件商品,成本价是80元,售价是100元。老板先是通知小李,说你去把商品价格增加50元。小李正在玩游戏,耽搁了一个小时。正好一个小时后,老板觉得商品价格增加到150元,价格太高,可能会影响销量。又通知小王,你把商品价格降低30元。
此时,小李和小王同时操作商品后台系统。小李操作的时候,系统先取出商品价格100元;小王 也在操作,取出的商品价格也是100元。小李将价格加了50元,并将100+50=150元存入了数据 库;小王将商品减了30元,并将100-30=70元存入了数据库。是的,如果没有锁,小李的操作就 完全被小王的覆盖了。
现在商品价格是70元,比成本价低10元。几分钟后,这个商品很快出售了1千多件商品,老板亏1 万多。
上面的故事,如果是乐观锁,小王保存价格前,会检查下价格是否被人修改过了。如果被修改过 了,则重新取出的被修改后的价格, 150元,这样他会将120元存入数据库。
如果是悲观锁,小李取出数据后,小王只能等小李操作完之后,才能对价格进行操作,也会保证最终的价格是120元。
②模拟修改冲突
数据库中添加商品表和数据
CREATE TABLEt_product
(
id BIGINT(20) NOT NULL COMMENT '主键ID',
NAME VARCHAR(30) NULL DEFAULT NULL COMMENT '商品名称 ',
price INT(11) DEFAULT 0 COMMENT '价格 ',
VERSION INT(11) DEFAULT 0 COMMENT '乐观锁版本号 ',
PRIMARY KEY (id));
INSERT INTO t_product (id, NAME, price) VALUES (1, '外星人笔记本 ', 100);
添加实体
package com.qcby.mybatisplus.model;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("t_product")
public class Product {
private Long id;
private String name;
private Integer price;
private Integer version;
}
添加mapper
package com.qcby.mybatisplus.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.qcby.mybatisplus.model.Product;
import org.springframework.stereotype.Repository;
@Repository //用于标识数据访问对象
public interface ProductMapper extends BaseMapper<Product> {
}
测试
@Test
public void testConcurrentUpdate() {
//1、小李
Product p1 = productMapper.selectById(1L);
System.out.println("小李取出的价格:" + p1.getPrice());
//2、小王
Product p2 = productMapper.selectById(1L);
System.out.println("小王取出的价格:" + p2.getPrice());
//3、小李将价格加了50元,存入了数据库
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1);
System.out.println("小李修改结果:" + result1);
//4、小王将商品减了30元,存入了数据库
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("小王修改结果:" + result2);
//最后的结果
Product p3 = productMapper.selectById(1L);
//价格覆盖,最后的结果:70
System.out.println("最后的结果:" + p3.getPrice());
}
输出结果
小李取出的价格:100
小王取出的价格:100
小李修改结果:1
小王修改结果:1
最后的结果:70
③乐观锁实现流程
数据库中添加version字段,取出记录时,获取当前version
SELECT id,`name`,price,`version` FROM product WHERE id=1
更新时, version + 1,如果where语句中的version版本不对,则更新失败
UPDATE product SET price=price+50, `version`=`version` + 1 WHERE id=1 AND `version`=1
④Mybatis-Plus实现乐观锁
修改实体类,添加@Version注解,指定用作其乐观锁定值的实体类的版本字段或属性
@Data
@TableName("t_product")
public class Product {
private Long id;
private String name;
private Integer price;
@Version
private Integer version;
}
添加乐观锁配置插件
@Bean
public MybatisPlusInterceptormybatisPlusInterceptor(){
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
//添加分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
//添加乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
return interceptor;
}
测试修改冲突流程
小李查询商品信息:
SELECT id,name,price,version FROM t_productWHERE id=?
小王查询商品信息:
SELECT id,name,price,version FROM t_productWHERE id=?
小李修改商品价格,自动将version+1
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters: 外星人笔记本(String), 150(Integer), 1(Integer), 1(Long), 0(Integer)
小王修改商品价格,此时version已更新,条件不成立,修改失败
UPDATE t_product SET name=?, price=?, version=? WHERE id=? AND version=? Parameters: 外星人笔记本(String), 70(Integer), 1(Integer), 1(Long), 0(Integer)
最终,小王修改失败,查询价格:
150 SELECT id,name,price,version FROM t_product WHERE id=?
优化流程
@Test
public void testConcurrentVersionUpdate() {
//小李取数据
Product p1 = productMapper.selectById(1L);
//小王取数据
Product p2 = productMapper.selectById(1L);
//小李修改 + 50
p1.setPrice(p1.getPrice() + 50);
int result1 = productMapper.updateById(p1);
System.out.println("小李修改的结果:" + result1);
//小王修改 - 30
p2.setPrice(p2.getPrice() - 30);
int result2 = productMapper.updateById(p2);
System.out.println("小王修改的结果:" + result2);
if(result2 == 0){
//失败重试,重新获取version并更新
p2 = productMapper.selectById(1L);
p2.setPrice(p2.getPrice() - 30);
result2 = productMapper.updateById(p2);
}
System.out.println("小王修改重试的结果:" + result2);
//老板看价格
Product p3 = productMapper.selectById(1L);
System.out.println("老板看价格:" + p3.getPrice());
}
输出结果
小李修改的结果:1
小王修改的结果:0
小王修改重试的结果:1
老板看价格:120
二、雪花算法
雪花算法(Snowflake)是Twitter开源的一种生成分布式全局唯一ID的算法。其核心思想是使用一个64位的long型数字作为全局唯一ID,这个数字由四部分组成:
- 第一位是符号位,由于生成的ID一般都是正数,所以这一位固定为0。
- 接下来的41位用来保存时间戳的差值(当前时间截 - 开始时间截),可以表示在自选定的时期以来的毫秒数。
- 接下来的10位用来表示工作机器ID,其中前5位是机房号,最多可以表示32个机房;后5位是机器ID,最多可以表示32台机器。
- 最后12位是一个序列号,用来表示同一毫秒内产生的不同ID。
雪花算法的优点在于:
- 生成的ID按时间递增,保证了ID的顺序性。
- 生成的ID在分布式系统中是唯一的,避免了ID冲突的问题。
- ID的生成不依赖于数据库,完全在内存中生成,因此性能高且可用。
- 每秒中可以生成数百万的自增ID,适用于高并发的分布式环境。
三、数据库的扩展方式:业务分库、主从复制和数据库分表。
- 业务分库:业务分库是根据业务模块将不同的数据分散存储到不同的数据库服务器。这种方式能够支撑百万甚至千万用户规模的业务。通过将不同的业务数据进行隔离,可以降低单一数据库的负载,提高系统的整体性能和可靠性。同时,业务分库也便于进行数据库的管理和维护。
- 主从复制:主从复制是一种常用的数据库扩展方式,主要通过将数据从主库复制到从库来实现。主库负责写操作,从库负责读操作。这种方式可以有效分摊数据库的读写压力,提高系统的吞吐量和性能。同时,主从复制还可以实现数据的备份和恢复,提高数据的可靠性和安全性。
- 数据库分表:数据库分表是将一张表的数据拆分到多张表中,以降低单张表的存储和查询压力。单表数据拆分有两种方式:垂直分表和水平分表。垂直分表是按照列进行拆分,将不同业务模块的数据分散到不同的表中;水平分表是按照行进行拆分,将同一业务模块的数据按照某种规则分散到多张表中。通过数据库分表,可以有效解决单表数据量过大导致的性能瓶颈问题,提高系统的可扩展性和性能。