一、初识分布式事务
以往业务开发中,项目架构往往是单体架构,随着时代的进步,业务量的增长,单体架构的项目往往会变得越来越臃肿,难以维护,甚至说一个节点崩了,可能会导致怎么项目的崩溃,为了解决这种问题,逐渐延伸出一种垂直拆分的架构体系,在单体系统的基础上集群部署多个单体应用,用nginx来做负载均衡分发请求到这两个应用中,这便是垂直应用架构。
当然,在扩展成分布式架构时,我们还需要解决分布式session问题,数据一致性问题也就是分布式事务,这个我后面再说。
在对单体项目进行垂直升级后,你会发现,同样的业务模块,登陆系统等等都被镜像的复制成了多份,导致有很多功能上的冗余,占用了不少内存,做了同样的事。
然后,我们会对重复的代码抽离出来,做成统一的服务,供其它服务或者业务模块调用。这,便是分布式架构。在分布式架构中,我们会将系统整体拆分为服务层和表现层,也就是服务层去实现核心的业务逻辑,提供接口让表现层去编排、调度服务、对外(前端可视化)进行交互,在分布式架构中,随着业务量的日益增长、不断膨胀的服务,服务与服务之间的关系变的越来越复杂,调用关系链越来越多,服务与服务之间越渐耦合。
显然,这是要出大问题的,一个服务要是崩了,势必会影响整个服务链一起裂开…
这种时候,我们需要增加一个统一的调度中心对服务集群进行时实管理,这就是SOA(面向服务)架构。通过加入一个注册中心解决了各个服务之间依赖和调用关系以及实现了主动注册、服务发现。
二、微服务架构(摘自书内)
微服务架构是在SOA架构的基础上进行进一步的扩展和拆分。在微服务架构下,一个大的项目拆分为一个个小的可独立部署的微服务,每个微服务都有自己的数据库。
这种架构的优点如下:
1.服务彻底拆分,各服务独立打包、独立部署和独立升级。
2.每个微服务负责的业务比较清晰,利于后期扩展和维护。
3.微服务之间可以采用REST和RPC协议进行通信。
微服务架构也是当下最主流最热门的架构。
这种架构的缺点如下
1.开发成本比较高
2.设计服务的容错性问题
3.设计数据的一致性问题
4.涉及分布式事务问题
三、分布式事务
从一个简单得例子引入分布式事务,创建springboot工程
maven依赖:
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
<!-- redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.2.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
库存扣减接口
@RestController
public class StockController {
@Resource
private StringRedisTemplate stringRedisTemplate;
@RequestMapping("/deduct_stock")
public String deductStock() {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
// 相当于 jedis.set(key.value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足");
}
return "end";
}
}
配置文件 application.yml
server:
port: 8090
redis:
database: 0
host: localhost
port: 6379
password:
timeout: 1000
jedis:
pool:
max-active: 8
min-idle: -1
max-idle: 8
max-wait: -1
配置nginx负载均衡,nginx可以参考我的另一篇文章:https://editor.csdn.net/md/?articleId=119704900
一个简单的负载均衡配置
worker_processes 1;
events {
worker_connections 1024;
}
http {
include mime.types;
default_type application/octet-stream;
sendfile on;
upstream redisLock{
server 127.0.0.1:8090 weight=1;
server 127.0.0.1:8091 weight=1;
}
server {
listen 80;
server_name localhost;
location / {
root html;
index index.html index.htm;
proxy_pass http://redisLock;
}
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}
}
}
允许多实例启动
启动8090实例
启动8091实例
redis desktop manager 工具设定redis键值:set “stock” “200”
查看值
模拟分布式应用高并发秒杀场景,改善现存在的并发问题,先上锁,现在,它们看起来貌似是安全的了,但是synchronized底层是基于JVM的,在高并发场景中,这样做还是会存在问题的
@RequestMapping("/deduct_stock")
public String deductStock() {
synchronized (this){
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
// 相当于 jedis.set(key.value)
stringRedisTemplate.opsForValue().set("stock", realStock + "");
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足");
}
}
return "end";
}
在官网下载jmeter进行压力测试,直接运行bat,添加线程组
添加http请求
设定好http配置,点击绿色按钮进行测试
记得刚刚改过代码,需要重启,结果如下
8090实例
8091实例
好了,看了上面的例子你悟了吗,你有啥感想嘛,没错!我们在当前程序中定义的变量最终是存储在当前程序中的JVM内存中的,如果我们是分布式部署应用,那么这两个变量的修改是不可见的,也就是并发编程三大特性,可见性,这时候我们需要一把分布式锁来对数据进行统一的管理。
分布式锁的实现方式列举三种,一种是通过mysql轻量级排他锁,select xxx where xxx for update,(都说了是轻量级,这种在高并发场景下是有问题的)
第二是通过redis实现分布式锁,第三种是基于zookeeper配置实现的分布式锁。
下面我们来讲讲redis实现分布式锁
使用命令介绍:
(1)SETNX
SETNX key val:当且仅当key不存在时,set一个key为val的字符串,返回1;若key存在,则什么都不做,返回0。
(2)expire
expire key timeout:为key设置一个超时时间,单位为second,超过这个时间锁会自动释放,避免死锁。
(3)delete
delete key:删除key
@RequestMapping("/deduct_stock")
public String deductStock() {
String lockKey = "lockKey";
String clientId = UUID.randomUUID().toString();
// 自定义一把分布式锁,依赖于redis的原子性,并发情况下也是单线程逐条执行,时效为10秒
Boolean result = stringRedisTemplate.opsForValue().setIfAbsent(lockKey, clientId, 10, TimeUnit.MINUTES);
if (!result) {
return "error_code";
}
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
// 相当于 jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足");
}
}finally {
// 这样来玩,也是会有问题的,如果说,程序在这一步获取成功锁后卡顿或者其它原因没有原子执行删除,此时因为缓存失效,到时锁被另一线程持有
// 就会出现删除了其它线程锁的问题,那么,怎么保证原子执行呢?
if (clientId.equals(stringRedisTemplate.opsForValue().get(lockKey))){
stringRedisTemplate.delete(lockKey);
}
}
return "end";
}
查看执行结果,舒服了…
实例1
实例2
刚刚的程序这样来玩,也是会有问题的,如果说,程序在这一步获取成功锁后卡顿或者其它原因没有原子执行删除,此时因为缓存失效,到时锁被另一线程持有
就会出现删除了其它线程锁的问题,那么,怎么保证原子执行呢?
1.可以让判断和删除原子执行,具体怎么做也有很多做法吧,也可以利用lua脚本去实现
2.在执行try代码块时,分离出一个线程去判断,当前线程是否还持有这把锁,如果持有,重新给缓存赋值,也就是续命锁,有点类似刷新token的操作
3.引入redisson(这玩意和jedis差不多,是redis在分布式场景下的一个封装框架)
在一开始的依赖相信你也看到了它
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.16.3</version>
</dependency>
配置redisson注入
@SpringBootApplication
public class ShopApplication {
public static void main(String[] args) {
SpringApplication.run(ShopApplication.class, args);
}
@Bean
public Redisson redisson() {
// 此为单机模式
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379").setDatabase(0);
return (Redisson) Redisson.create(config);
}
}
api新写一个接口,确实去除了不少冗余代码呀
@RequestMapping("/deduct_stock2")
public String deductStock2() {
String lockKey = "lockKey";
// 设定锁键值,超时失效,原子性,有且只有一个线程执行下面的逻辑,没拿到锁就阻塞,和续命锁的逻辑,续命时间为超时时间的三分之一
RLock lock = redisson.getLock(lockKey);
lock.lock();
try {
int stock = Integer.parseInt(stringRedisTemplate.opsForValue().get("stock"));
if (stock > 0) {
int realStock = stock - 1;
stringRedisTemplate.opsForValue().set("stock",realStock+"");
// 相当于 jedis.set(key,value)
System.out.println("扣减成功,剩余库存:" + realStock);
} else {
System.out.println("扣减库存失败,库存不足");
}
}finally {
lock.unlock();
}
return "end";
}
四、微服务的去中心化治理
以前,我以为的微服务是集成了服务注册中心、负载均衡、网关、熔断等等的项目架构,然后,俺看了书,大佬在讲项目架构的时候,拆分了几个阶段,单体应用架构、垂直应用架构、分布式架构、SOA(面向服务)架构、微服务架构,大佬说,SOA(面向服务)架构是在分布式架构上加上了服务注册中心做服务管理与发现,而在微服务架构讲的是业务独立拆分成服务部署,没有又说引入注册中心哪些组件呀,然后通过rpc、rest进行服务间的通信,这让俺十分困惑。到底,什么才是微服务?我不得而知,突然想起了大佬在库存扣减业务中常说的去中心化,难道,这才是真正的微服务思想吗?
转载:https://www.cnblogs.com/zhao-teng-ass/p/11055871.html
随着主体对客体的相互作用的深入和认知机能的不断平衡、认知结构的不断完善,个体能从自我中心状态中解除出来,皮亚杰称之为去中心化。
当平台的决策者倡导建设API网关,所有外部服务和内部服务都由统一的API网关进行管理。在项目初期,中心化的API网关统一了所有API的入口,这看起来很规范,但从技术角度来看限制了API的多样化。随着业务的发展,API网关开始暴露问题,每个用户请求经过机房时只要有服务之间的交互,则都会从API网关进行路由,服务上量以后,由于内部服务之间的交互都会叠加在API网关的调用上,所以在很大程度上放大了API网关的调用TPS,API网关很快就遇到了性能瓶颈。
上面这个案例是典型的微服务的反模式,微服务倡导去中心化的治理,不推荐每个微服务都使用相同的标准和技术来开发和使用服务。
在微服务架构下可以使用C++开发一个服务,来对接Java开发的另外一个服务,对于异构系统之间的交互标准,通常可以使用工具来补偿。开发者可以开发共用的工具,并分享给异构系统的开发者使用,来解决异构系统不一致的问题,例如:Thrift远程调用框架使用中间语言(IDL)来定义接口,中间语言是独立于任何语言的,并提供了工具来生成中间语言,以及在中间语言与具体语言之间的代码转换。
微服务架构倡导去中心化的服务管理和治理,尽量不设置中心化的管理服务,最差也需要在中心化的管理服务宕机时有替代方案和设计。在支付平台服务化建设中,如果第1层SOA服务化采用Dubbo框架进行定制化,如果Dubbo服务化出现了大面积的崩溃,则服务化体系会切换到点对点的hessian远程调用,这被称为服务化降级,降级后点对点的hessian远程调用时没有中心化节点,整体上符合微服务的原理。
五、最后
分布式思想为我打开了一面全新的大门,微服务仍然令我困惑,幸好,我有先见之明,准备翻开我的另一本书,相信看了它我应该能得到我想要的答案 …
结语:
站在巨人的肩膀上,我看到了更美的风景,我也想将此风景写给更多的人看
六、推荐
推推冰河大佬写的书啦