业务幂等性技术架构体系

业务幂等性技术架构体系
1)幂等性介绍
现如今很多系统都会基于分布式或微服务思想完成对系统的
架构设计。那么在这一个系统中,就会存在若干个微服务,
而且服务间也会产生相互通信调用。那么既然产生了服务调
用,就必然会存在服务调用延迟或失败的问题。当出现这种
问题,服务端会进行重试等操作或客户端有可能会进行多次
点击提交。如果这样请求多次的话,那最终处理的数据结果
就一定要保证统一,如支付场景。此时就需要通过保证业务
幂等性方案来完成。
1.1)简介
幂等本身是一个数学概念。即f(n) = 1^n,无论n为多少,
f(n)的值永远为1。在编程开发中,对于幂等的定义为:无论
对某一个资源操作了多少次,其影响都应是相同的。 换句话
说就是:在接口重复调用的情况下,对系统产生的影响是一
样的,但是返回值允许不同,如查询。
幂等性不仅仅只是一次或多次操作对资源没有产生影响,还
包括第一次操作产生影响后,以后多次操作不会再产生影
响。并且幂等关注的是是否对资源产生影响,而不关注结
果。
以SQL为例:select * from table where id=1。此SQL无论执行多
少次,虽然结果有可能出现不同,都不会对数据产生改
变,具备幂等性。
insert into table(id,name) values(1,'heima')。
此SQL如果id或name有唯一性约束,多次操作只允许插入
一条记录,则具备幂等性。如果不是,则不具备幂等性,
多次操作会产生多条数据。
update table set score=100 where id = 1。此SQL
无论执行多少次,对数据产生的影响都是相同的。具备幂
等性。
update table set score=50+score where id = 1。
此SQL涉及到了计算,每次操作对数据都会产生影响。不
具备幂等性。
delete from table where id = 1。此SQL多次操作,
产生的结果相同,具备幂等性。
幂等性设计主要从两个维度进行考虑:空间、时间。
空间:定义了幂等的范围,如生成订单的话,不允许出现
重复下单。
时间:定义幂等的有效期。有些业务需要永久性保证幂
等,如下单、支付等。而部分业务只要保证一段时间幂等
即可。
同时对于幂等的使用一般都会伴随着出现锁的概念,用于解
决并发安全问题。
1.2)业务问题抛出在业务开发与分布式系统设计中,幂等性是一个非常重要的
概念,有非常多的场景需要考虑幂等性的问题,尤其对于现
在的分布式系统,经常性的考虑重试、重发等操作,一旦产
生这些操作,则必须要考虑幂等性问题。以交易系统、支付
系统等尤其明显,如:
当用户购物进行下单操作,用户操作多次,但订单系统对
于本次操作只能产生一个订单。
当用户对订单进行付款,支付系统不管出现什么问题,应
该只对用户扣一次款。
当支付成功对库存扣减时,库存系统对订单中商品的库存
数量也只能扣减一次。
当对商品进行发货时,也需保证物流系统有且只能发一次
货。
在电商系统中还有非常多的场景需要保证幂等性。但是一旦
考虑幂等后,服务逻辑务必会变的更加复杂。因此是否要考
虑幂等,需要根据具体业务场景具体分析。而且在实现幂等
时,还会把并行执行的功能改为串行化,降低了执行效率。
此处以下单减库存为例,当用户生成订单成功后,会对订单
中商品进行扣减库存。 订单服务会调用库存服务进行库存扣
减。库存服务会完成具体扣减实现。
现在对于功能调用的设计,有可能出现调用超时,因为出现
如网络抖动,虽然库存服务执行成功了,但结果并没有在超
时时间内返回,则订单服务也会进行重试。那就会出现问
题,stock对于之前的执行已经成功了,只是结果没有按时返
回。而订单服务又重新发起请求对商品进行库存扣减。 此时
出现库存扣减两次的问题。 对于这种问题,就需要通过幂等
性进行结果。1.4)HTTP协议语义幂等性
HTTP协议有两种方式:RESTFUL、SOA。现在对于WEB
API,更多的会使用RESTFUL风格定义。为了更好的完成接口
语义定义,HTTP对于常用的四种请求方式也定义了幂等性的
语义。
GET:用于获取资源,多次操作不会对数据产生影响,具
有幂等性。注意不是结果。
POST:用于新增资源,对同一个URI进行两次POST操作
会在服务端创建两个资源,不具有幂等性。
PUT:用于修改资源,对同一个URI进行多次PUT操作,产
生的影响和第一次相同,具备幂等性。
DELETE:用于删除资源,对同一个URI进行多次DELETE
操作,产生的影响和第一次相同,具备幂等性。
综上所述,这些仅仅只是HTTP协议建议在基于RESTFUL风格
定义WEB API时的语义,并非强制性。同时对于幂等性的实
现,肯定是通过前端或服务端完成。
2)接口幂等
对于幂等的考虑,主要解决两点前后端交互与服务间交互。
这两点有时都要考虑幂等性的实现。从前端的思路解决的
话,主要有三种:前端防重、PRG模式、Token机制。
2.1)前端防重
通过前端防重保证幂等是最简单的实现方式,前端相关属性
和JS代码即可完成设置。可靠性并不好,有经验的人员可以
通过工具跳过页面仍能重复提交。主要适用于表单重复提交
或按钮重复点击。
2.2)PRG模式
PRG模式即POST-REDIRECT-GET。当用户进行表单提交时,
会重定向到另外一个提交成功页面,而不是停留在原先的表
单页面。这样就避免了用户刷新导致重复提交。同时防止了
通过浏览器按钮前进/后退导致表单重复提交。是一种比较常
见的前端防重策略。
2.3)token机制
2.3.1)方案介绍
通过token机制来保证幂等是一种非常常见的解决方案,同时
也适合绝大部分场景。该方案需要前后端进行一定程度的交
互来完成。1)服务端提供获取token接口,供客户端进行使用。服务端
生成token后,如果当前为分布式架构,将token存放于redis
中,如果是单体架构,可以保存在jvm缓存中。
2)当客户端获取到token后,会携带着token发起请求。
3)服务端接收到客户端请求后,首先会判断该token在redis
中是否存在。如果存在,则完成进行业务处理,业务处理完
成后,再删除token。如果不存在,代表当前请求是重复请
求,直接向客户端返回对应标识。
但是现在有一个问题,当前是先执行业务再删除token。在
高并发下,很有可能出现第一次访问时token存在,完成具体
业务操作。但在还没有删除token时,客户端又携带token发
起请求,此时,因为token还存在,第二次请求也会验证通
过,执行具体业务操作。对于这个问题的解决方案的思想就是并行变串行。会造成一
定性能损耗与吞吐量降低。
第一种方案:对于业务代码执行和删除token整体加线程锁。
当后续线程再来访问时,则阻塞排队。
第二种方案:借助redis单线程和incr是原子性的特点。当第
一次获取token时,以token作为key,对其进行自增。然后
将token进行返回,当客户端携带token访问执行业务代码
时,对于判断token是否存在不用删除,而是对其继续incr。
如果incr后的返回值为2。则是一个合法请求允许执行,如果
是其他值,则代表是非法请求,直接返回。
那如果先删除token再执行业务呢?其实也会存在问题,假
设具体业务代码执行超时或失败,没有向客户端返回明确结
果,那客户端就很有可能会进行重试,但此时之前的token已
经被删除了,则会被认为是重复请求,不再进行业务处理。这种方案无需进行额外处理,一个token只能代表一次请求。
一旦业务执行出现异常,则让客户端重新获取令牌,重新发
起一次访问即可。推荐使用先删除token方案
但是无论先删token还是后删token,都会有一个相同的问
题。每次业务请求都回产生一个额外的请求去获取token。但
是,业务失败或超时,在生产环境下,一万个里最多也就十
个左右会失败,那为了这十来个请求,让其他九千九百多个
请求都产生额外请求,就有一些得不偿失了。虽然redis性能
好,但是这也是一种资源的浪费。
2.3.2)实现
2.3.2.1)基于自定义业务流程实现1)修改token_service_order工程中OrderController,新
增生成令牌方法genToken
1 @Autowired
2 private IdWorker idWorker;
3
4 @Autowired
5 private RedisTemplate redisTemplate;
6
7 @GetMapping("/genToken")
8 public String genToken(){
9
10 String token =
String.valueOf(idWorker.nextId());
11
12
redisTemplate.opsForValue().set(token,0,30,
TimeUnit.MINUTES);
13
14 return token;
15 }
2)修改token_service_api工程,新增OrderFeign接口。1 @FeignClient(name = "order")
2 @RequestMapping("/order")
3 public interface OrderFeign {
4
5 @GetMapping("/genToken")
6 public String genToken();
7 }
3)修改token_web_order工程中WebOrderController,
新增获取token方法
1 @RestController
2 @RequestMapping("worder")
3 public class WebOrderController {
4
5 @Autowired
6 private OrderFeign orderFeign;
7
8 /**
9 * 服务端生成token
10 * @return
11 */
12 @GetMapping("/genToken")
13 public String genToken(){
14
15 String token =
orderFeign.genToken();
16
17 return token;
18 }
19
20 }4)修改token_common,新增feign拦截器
1 @Component
2 public class FeignInterceptor implements
RequestInterceptor {
3
4 @Override
5 public void apply(RequestTemplate
requestTemplate) {
6
7 //传递令牌
8 RequestAttributes requestAttributes
=
RequestContextHolder.getRequestAttributes();
9
10 if (requestAttributes != null){
11
12 HttpServletRequest request =
((ServletRequestAttributes)
requestAttributes).getRequest();
13
14 if (request != null){
15
16 Enumeration<String>
headerNames = request.getHeaderNames();
17
18 while
(headerNames.hasMoreElements()){
19
20 String headerName =
headerNames.nextElement();
2122 if
("token".equals(headerName)){
23
24 String headerValue =
request.getHeader(headerName);
25
26 //传递token
27
requestTemplate.header(headerName,headerVal
ue);
28 }
29 }
30 }
31 }
32 }
33 }
5)修改token_web_order启动类
1 @Bean
2 public FeignInterceptor feignInterceptor(){
3 return new FeignInterceptor();
4 }
6)修改token_service_order中OrderController,新增添
加订单方法
1 /**
2 * 生成订单
3 * @param order
4 * @return
5 */
6 @PostMapping("/genOrder")7 public String genOrder(@RequestBody Order
order, HttpServletRequest request){
8
9 //获取令牌
10 String token =
request.getHeader("token");
11
12 //校验令牌
13 try {
14 if (redisTemplate.delete(token)){
15
16 //令牌删除成功,代表不是重复请求,执行
具体业务
17
order.setId(String.valueOf(idWorker.nextId(
)));
18 order.setCreateTime(new Date());
19 order.setUpdateTime(new Date());
20 int result =
orderService.addOrder(order);
21
22 if (result == 1){
23
System.out.println("success");
24 return "success";
25 }else {
26 System.out.println("fail");
27 return "fail";
28 }
29 }else {
30
31 //删除令牌失败,重复请求32 System.out.println("repeat
request");
33 return "repeat request";
34 }
35 }catch (Exception e){
36 throw new RuntimeException("系统异常,
请重试");
37 }
38 }
7)修改token_service_order_api中OrderFeign。
1 @FeignClient(name = "order")
2 @RequestMapping("/order")
3 public interface OrderFeign {
4
5 @PostMapping("/genOrder")
6 public String genOrder(@RequestBody
Order order);
7
8 @GetMapping("/genToken")
9 public String genToken();
10 }
8)修改token_web_order中WebOrderController,新增
添加订单方法9)测试
通过postman获取令牌,将令牌放入请求头中。开启两个
postman tab页面。同时添加订单,可以发现一个执行成
功,另一个重复请求。
2.3.2.2)基于自定义注解实现
直接把token实现嵌入到方法中会造成大量重复代码的出现。
因此可以通过自定义注解将上述代码进行改造。在需要保证
幂等的方法上,添加自定义注解即可。
1)在token_common中新建自定义注解Idemptent
/**
* 新增订单
*/
@PostMapping("/addOrder")
public String addOrder(@RequestBody Order
order){
String result =
orderFeign.genOrder(order);
return result;
}
1
2
3
4
5
6
7
8
9
10
{"id":"123321","totalNum":1,"payMoney":1,"pay
Type":"1","payTime":"2020-05-
20","receiverContact":"heima","receiverMobile
":"15666666666","receiverAddress":"beijing"}
11 /**
2 * 幂等性注解
3 */
4 @Target({ElementType.METHOD})
5 @Retention(RetentionPolicy.RUNTIME)
6 public @interface Idemptent {
7 }
2)在token_common中新建拦截器
1 public class IdemptentInterceptor implements
HandlerInterceptor {
2
3 @Override
4 public boolean
preHandle(HttpServletRequest request,
HttpServletResponse response, Object
handler) throws Exception {
5
6 if (!(handler instanceof
HandlerMethod)) {
7 return true;
8 }
9
10 HandlerMethod handlerMethod =
(HandlerMethod) handler;
11 Method method =
handlerMethod.getMethod();
12
13 Idemptent annotation =
method.getAnnotation(Idemptent.class);
14 if (annotation != null){
15 //进行幂等性校验16 checkToken(request);
17 }
18
19 return true;
20 }
21
22
23 @Autowired
24 private RedisTemplate redisTemplate;
25
26 //幂等性校验
27 private void
checkToken(HttpServletRequest request) {
28 String token =
request.getHeader("token");
29 if (StringUtils.isEmpty(token)){
30 throw new RuntimeException("非法
参数");
31 }
32
33 boolean delResult =
redisTemplate.delete(token);
34 if (!delResult){
35 //删除失败
36 throw new RuntimeException("重复
请求");
37 }
38 }
39
40 @Override41 public void
postHandle(HttpServletRequest request,
HttpServletResponse response, Object
handler, ModelAndView modelAndView) throws
Exception {
42
43 }
44
45 @Override
46 public void
afterCompletion(HttpServletRequest request,
HttpServletResponse response, Object
handler, Exception ex) throws Exception {
47
48 }
49 }
3)修改token_service_order启动类,让其继承
WebMvcConfigurerAdapter1 @Bean
2 public IdemptentInterceptor
idemptentInterceptor() {
3 return new IdemptentInterceptor();
4 }
5
6 @Override
7 public void
addInterceptors(InterceptorRegistry
registry) {
8 //幂等拦截器
9
registry.addInterceptor(idemptentIntercepto
r());
10 super.addInterceptors(registry);
11 }
4)更新token_service_order与
token_service_order_api,新增添加订单方法,并且方法
添加自定义幂等注解
1 @Idemptent
2 @PostMapping("/genOrder2")
3 public String genOrder2(@RequestBody Order
order){
4
5
order.setId(String.valueOf(idWorker.nextId(
)));
6 order.setCreateTime(new Date());
7 order.setUpdateTime(new Date());8 int result =
orderService.addOrder(order);
9
10 if (result == 1){
11 System.out.println("success");
12 return "success";
13 }else {
14 System.out.println("fail");
15 return "fail";
16 }
17 }
4)测试
获取令牌后,在jemeter中模拟高并发访问
设置50个并发访问
新增一个http request。并设置相关信息添加HTTP Header Manager
测试执行,可以发现,只有一个请求是成功的,其他全部被
判定为重复请求。
3)服务幂等
3.1)防重表
对于防止数据重复提交,还有一种解决方案就是通过防重表
实现。防重表的实现思路也非常简单。首先创建一张表作为
防重表,同时在该表中建立一个或多个字段的唯一索引作为
防重字段,用于保证并发情况下,数据只有一条。在向业务
表中插入数据之前先向防重表插入,如果插入失败则表示是
重复数据。对于防重表的解决方案,可能有人会说为什么不使用悲观
锁。悲观锁在使用的过程中也是会发生死锁的。悲观锁是通
过锁表的方式实现的。 假设现在一个用户A访问表A(锁住了
表A),然后试图访问表B; 另一个用户B访问表B(锁住了
表B),然后试图访问表A。 这时对于用户A来说,由于表B
已经被用户B锁住了,所以用户A必须等到用户B释放表B才能
访问。 同时对于用户B来说,由于表A已经被用户A锁住了,
所以用户B必须等到用户A释放表A才能访问。此时死锁就已
经产生了。
3.2)select+insert防重提交
对于一些后台系统,并发量并不高的情况下,对于幂等的实
现非常简单,通过select+insert思想即可完成幂等控制。
在业务执行前,先判断是否已经操作过,如果没有则执行,
否则判断为重复操作。3.2.1)效果演示
在并发下访问时,因为是基于id进行判断,那id值就必须要保
证在多次提交时,需要唯一。访问流程如下:1 @Override
2 @Transactional(rollbackFor =
Exception.class)
3 public String addOrder(Order order) {
4
5 order.setCreateTime(new Date());
6 order.setUpdateTime(new Date());
7
8 //查询对于上述功能实现,在并发下,并不能完成幂等性控制。通
过jemeter测试,模拟50个并发,可以发现,插入了重复数
据。产生了脏数据。
要解决这个问题,非常简单,在数据库层面添加唯一索引即
可,将id设置为唯一索引,也是最容易想到的方式,一旦id出
现重复,就会出现异常,避免了脏数据的发生也可以解决永
久性幂等。但该方案无法用于分库分表情况,其只适用于单
表情况。
3.3)MySQL乐观锁
Order orderResult =
orderMapper.selectByPrimaryKey(order.getId()
);
Optional<Order> orderOptional =
Optional.ofNullable(orderResult);
if (orderOptional.isPresent()){
return "repeat request";
}
int result = orderMapper.insert(order);
if (result != 1){
return "fail";
}
return "success";
}
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23假设现在订单已经生成成功,那么就会涉及到扣减库存的操
作。当高并发下同时扣减库存时,非常容易出现数据错误问
题。
3.3.1)扣减库存数据错误
导入idempotent_optimistic工程,通过jemeter进行测
试,可以发现。当模拟一万并发时,最终的库存数量是错误
的。这主要是因为当多线程访问时,一个线程读取到了另外
线程未提交的数据造成。3.3.2)synchronized失效问题
对于现在的问题,暂不考虑秒杀设计、队列请求串行化等,
只考虑如何通过锁进行解决,要通过锁解决的话,那最先想
到的可能是synchronized。根据synchronized定义,当多
线程并发访问时,会对当前加锁的方法产生阻塞,从而保证
线程安全,避免脏数据。但是,真的能如预期的一样吗?
当前已经在在方法上添加了synchronized,对当前方法对象
进行了锁定。 通过Jemeter,模拟一万并发对其进行访问。
可以发现,仍然出现了脏数据。
@Service
public class StockServiceImpl implements
StockService {
@Autowired
private StockMapper stockMapper;
@Override
@Transactional(rollbackFor =
Exception.class)
public synchronized int
lessInventory(String goodsId, int num) {
return
stockMapper.lessInventory(goodsId, num);
}
}
1
2
3
4
5
6
7
8
9
10
11
12该问题的产生原因,就在于在方法上synchronized搭配使用
了@Transactional。首先synchronized锁定的是当前方法
对象,而@Transactional会对当前方法进行AOP增强,动态
代理出一个代理对象,在方法执行前开启事务,执行后提交
事务。 所以synchronized和@Transactional其实操作的是
两个不同的对象,换句话说就是@Transactional的事务操作
并不在synchronized锁定范围之内。
假设A线程执行完扣减库存方法,会释放锁并提交事务。但A
线程释放锁但还没提交事务前,B线程执行扣减库存方法,B
线程执行后,和A线程一起提交事务,就出现了线程安全问
题,造成脏数据的出现。
3.3.3)MySQL乐观锁保证幂等
3.3.3.1)基于版本号实现
MySQL乐观锁是基于数据库完成分布式锁的一种实现,实现
的方式有两种:基于版本号、基于条件。但是实现思想都是
基于MySQL的行锁思想来实现的。

1)修改数据表,添加version字段,默认值为0
2)修改StockMapper添加基于版本修改数据方法

1 @Update("update tb_stock set amount=amount-#
{num},version=version+1 where goods_id=#
{goodsId} and version=#{version}")
2 int lessInventoryByVersion(@Param("goodsId")
String goodsId,@Param("num") int
num,@Param("version") int version);
3)测试模拟一万并发进行数据修改,此时可以发现当前版本
号从0变为1,且库存量正确。

3.3.3.2)基于条件实现
通过版本号控制是一种非常常见的方式,适合于大多数场
景。但现在库存扣减的场景来说,通过版本号控制就是多人
并发访问购买时,查询时显示可以购买,但最终只有一个人
能成功,这也是不可以的。其实最终只要商品库存不发生超
卖就可以。那此时就可以通过条件来进行控制。
1)修改StockMapper:
2)修改StockController:
@Update("update tb_stock set amount=amount-#
{num} where goods_id=#{goodsId} and amount-#
{num}>=0")
int
lessInventoryByVersionOut(@Param("goodsId")
String goodsId,@Param("num") int num);
1
23)通过jemeter进行测试,可以发现当多人并发扣减库存
时,控制住了商品超卖的问题。
3.3.4)乐观锁控制服务间幂等
在系统中,不光要保证客户端访问的幂等性,同时还要保证
服务间幂等。比较常见的情况,当服务间进行调用时,因为
网络抖动等原因出现超时,则很有可能出现数据错误。此时
在分布式环境下,就需要通过分布式事务或分布式锁来保证
数据的一致性。分布式锁的解决方案中MySQL乐观锁就是其
中一种实现。
@PutMapping("/lessInventoryByVersionOut/{goo
dsId}/{num}")
public String
lessInventoryByVersionOut(@PathVariable("goo
dsId") String goodsId,@PathVariable("num")
int num){
int result =
stockService.lessInventoryByVersionOut(goods
Id, num);
if (result == 1){
System.out.println("购买成功");
return "success";
}
System.out.println("购买失败");
return "fail";
}
1
2
3
4
5
6
7
8
9
10
11
123.3.4.1)feign超时重试效果演示
以上图为例,当客户端要生成订单时,可以基于token机制保
证生成订单的幂等性,接着订单生成成功后,还会基于feign
调用库存服务进行库存扣减,此时则很有可能出现,库存服
务执行扣减库存成功,但是当结果返回时,出现网络抖动超
时了,那么上游的订单服务则很有可能会发起重试,此时如
果不进行扣减库存的幂等性保证的话,则出现扣减库存执行
多次。
那可以先来演示当下游服务出现延迟,上游服务基于feign进
行重试的效果。
1)导入idempotent_optimictic_server工程。
2)当前是order调用feign,所以在order中会存在
feignConfigure配置类,用于配置超时时间与重试次数。
/**
* 自定义feign超时时间、重试次数
* 默认超时为10秒,不会进行重试。
*/
@Configuration
public class FeignConfigure {
1
2
3
4
5
67
8 //超时时间,时间单位毫秒
9 public static int connectTimeOutMillis =
5000;
10 public static int readTimeOutMillis =
5000;
11
12 @Bean
13 public Request.Options options() {
14 return new
Request.Options(connectTimeOutMillis,
readTimeOutMillis);
15 }
16
17 //自定义重试次数
18 @Bean
19 public Retryer feignRetryer(){
20 Retryer retryer = new
Retryer.Default(100, 1000, 4);
21 return retryer;
22 }
23 }
3)stock服务的StockController中demo方法会延迟六秒。
通过这种方式模拟超时效果。此时在order中调用stock服
务,可以发现,order服务会对stock服务调用四次。这里就演示了服务间调用超时的效果,当下游服务超时,上
游服务会进行重试。
3.3.4.2)服务调用超时库存多次扣减
根据上述演示,当下游服务超时,上游服务就会进行重试。
那么结合当前的业务场景,当用户下单成功去调用库存服务
扣减库存时, 如果库存服务执行扣减库存成功但返回结果超
时,则上游订单服务就会重试,再次进行扣减库存,此时就
会出现同一订单商品库存被多次扣减。
1)在订单服务中生成订单,并调用库存服务扣减库存
@Idemptent
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order
order){
String orderId =
String.valueOf(idWorker.nextId());
order.setId(orderId);
order.setCreateTime(new Date());
order.setUpdateTime(new Date());
int result =
orderService.addOrder(order);
if (result != 1){
System.out.println("fail");
1
2
3
4
5
6
7
8
9
10
11
1213 return "fail";
14 }
15
16 //生成订单详情信息
17 List<String> goodsIdArray =
JSON.parseArray(order.getGoodsIds(),
String.class);
18
19 goodsIdArray.stream().forEach(goodsId->{
20 //插入订单详情
21 OrderDetail orderDetail = new
OrderDetail();
22
orderDetail.setId(String.valueOf(idWorker.n
extId()));
23 orderDetail.setGoodsId(goodsId);
24 orderDetail.setOrderId(orderId);
25 orderDetail.setGoodsName("heima");
26 orderDetail.setGoodsNum(1);
27 orderDetail.setGoodsPrice(1);
28
orderDetailService.addOrderDetail(orderDeta
il);
29
30 //扣减库存(不考虑锁)
31
stockFeign.reduceStockNoLock(goodsId,
orderDetail.getGoodsNum());
32
33 });
34
35
36 return "success";37 }
2)库存服务直接基于商品信息进行库存扣减
1 @Update("update tb_stock set amount=amount-#
{num} where goods_id=#{goodsId}")
2 int reduceStockNoLock(@Param("goodsId")
String goodsId,@Param("num") Integer num);
1 @PutMapping("/reduceStockNoLock/{goodsId}/{n
um}")
2 public String
reduceStockNoLock(@PathVariable("goodsId")
String goodsId,
3
@PathVariable("num") Integer num) throws
InterruptedException {
4
5 System.out.println("reduce stock");
6 int result =
stockService.reduceStockNoLock(goodsId,
num);
7
8 if (result != 1){
9 return "reduce stock fail";
10 }
11
12 //延迟
13 TimeUnit.SECONDS.sleep(6000);
14 return "reduce stock success";
15 }3)执行生成订单扣减库存,此时可以发现扣减库存方法被执
行多次,并且库存数量也被扣减了多次
3.3.4.3)乐观锁解决服务间重试保证幂等   看到
1)修改StockMapper,添加乐观锁控制控制库存
2)修改StockController,添加乐观锁扣减库存方法
{"totalNum":1,"payMoney":1,"goodsIds":"
['1271700536000909313']"}
1
@Update("update tb_stock set
version=version+1,amount=amount-#{num} where
goods_id=#{goodsId} and version=#{version}
and amount-#{num}>=0")
int reduceStock(@Param("goodsId") String
goodsId,@Param("num") Integer
num,@Param("version") Integer version);
1
2
/**
* 乐观锁扣减库存
1
23 * @param goodsId
4 * @param num
5 * @param version
6 * @return
7 */
8 @PutMapping("/reduceStock/{goodsId}/{num}/{v
ersion}")
9 public int
reduceStock(@PathVariable("goodsId") String
goodsId,
10 @PathVariable("num")
Integer num,
11
@PathVariable("version") Integer version)
throws InterruptedException {
12
13 System.out.println("exec reduce stock");
14 int result =
stockService.reduceStock(goodsId, num,
version);
15 if (result != 1){
16 //扣减失败
17 return result;
18 }
19 //延迟
20 TimeUnit.SECONDS.sleep(6000);
21 return result;
22 }
3)测试,可以发现虽然发生多次重试,但是库存只会被扣减
成功一次。保证了服务间的幂等性。ps:order服务出现异常,是因为order服务会超时重试四次,但
stock服务的延迟每一次都是超过超时时间的,所以最终在order
服务才会出现read timeout异常提示。
3.3.5)使用场景
mysql乐观锁更适用于一些需要计数的表上,而且在竞争不
激烈,出现并发冲突几率较小时,推荐使用乐观锁。虽然通
过MySQL乐观锁可以完成并发控制,但锁的操作是直接作用
于数据库上,这样就会在一定程度上对数据库性能产生影
响。并且mysql的连接数量是有限的,如果出现大量锁操作
占用连接时,也会造成MySQL的性能瓶颈。
3.4)zookeeper分布式锁
3.4.1)实现思想
对于分布式锁的实现,zookeeper天然携带的一些特性能够
很完美的实现分布式锁。其内部主要是利用znode节点特性
和watch机制完成。
3.4.1.1)znode节点
在zookeeper中节点会分为四类,分别是:
持久节点:一旦创建,则永久存在于zookeeper中,除非
手动删除。
持久有序节点:一旦创建,则永久存在于zookeeper中,
除非手动删除。同时每个节点都会默认存在节点序号,每
个节点的序号都是有序递增的。如demo000001、
demo000002.....demo00000N。临时节点:当节点创建后,一旦服务器重启或宕机,则被
自动删除。
临时有序节点:当节点创建后,一旦服务器重启或宕机,
则被自动删除。同时每个节点都会默认存在节点序号,每
个节点的序号都是有序递增的。如demo000001、
demo000002.....demo00000N。
3.4.1.2)watch监听机制
watch监听机制主要用于监听节点状态变更,用于后续事件
触发,假设当B节点监听A节点时,一旦A节点发生修改、删
除、子节点列表发生变更等事件,B节点则会收到A节点改变
的通知,接着完成其他额外事情。
3.4.1.3)实现原理其实现思想是当某个线程要对方法加锁时,首先会在
zookeeper中创建一个与当前方法对应的父节点,接着每个
要获取当前方法的锁的线程,都会在父节点下创建一个临时
有序节点,因为节点序号是递增的,所以后续要获取锁的线
程在zookeeper中的序号也是逐次递增的。根据这个特性,
当前序号最小的节点一定是首先要获取锁的线程,因此可以
规定序号最小的节点获得锁。所以,每个线程再要获取锁
时,可以判断自己的节点序号是否是最小的,如果是则获取
到锁。当释放锁时,只需将自己的临时有序节点删除即可。
根据上图,在并发下,每个线程都会在对应方法节点下创建
属于自己的临时节点,且每个节点都是临时且有序的。那么
zookeeper又是如何有序的将锁分配给不同线程呢? 这里就
应用到了watch监听机制。每当添加一个新的临时节点时,
其都会基于watcher机制监听着它本身的前一个节点等待前一
个节点的通知,当前一个节点删除时,就轮到它来持有锁
了。然后依次类推。1)zookeeper是基于cp模式,能够保证数据强一致性。
2)基于watch机制实现锁释放的自动监听,锁操作性能较
好。
3)频繁创建节点,对于zk服务器压力较大,吞吐量没有
redis强。
3.4.2)原理剖析&实现
3.4.2.1)低效锁思想&实现
在通过zookeeper实现分布式锁时,有另外一种实现的写
法,这种也是非常常见的,但是它的效率并不高,此处可以
先对这种实现方式进行探讨。此种实现方式,只会存在一个锁节点。当创建锁节点时,如
果锁节点不存在,则创建成功,代表当前线程获取到锁,如
果创建锁节点失败,代表已经有其他线程获取到锁,则该线
程会监听锁节点的释放。当锁节点释放后,则继续尝试创建
锁节点加锁。
3.4.2.1.1)实现
1)在zookeeper_common中创建抽象类AbstractLock
public abstract class AbstractLock {
//zookeeper服务器地址
public static final String
ZK_SERVER_ADDR="192.168.200.131:2181";
//zookeeper超时时间
public static final int
CONNECTION_TIME_OUT=30000;
1
2
3
4
5
6
78 public static final int
SESSION_TIME_OUT=30000;
9
10 //创建zk客户端
11 protected ZkClient zkClient = new
ZkClient(ZK_SERVER_ADDR,SESSION_TIME_OUT,CON
NECTION_TIME_OUT);
12
13 /**
14 * 获取锁
15 * @return
16 */
17 public abstract boolean tryLock();
18
19 /**
20 * 等待加锁
21 */
22 public abstract void waitLock();
23
24 /**
25 * 释放锁
26 */
27 public abstract void releaseLock();
28
29 public void getLock() {
30
31 String threadName =
Thread.currentThread().getName();
32
33 if (tryLock()) {
34 System.out.println(threadName+":
获取锁成功");
35 }else {36 System.out.println(threadName+":
获取锁失败,等待中");
37 //等待锁
38 waitLock();
39 getLock();
40 }
41 }
42 }
2)创建LowLock
1 public class LowLock extends AbstractLock{
2
3 private static final String
LOCK_NODE_NAME = "/lock_node";
4
5 private CountDownLatch countDownLatch;
6
7 @Override
8 public boolean tryLock() {
9 if (zkClient == null){
10 return false;
11 }
12 try {
13
zkClient.createEphemeral(LOCK_NODE_NAME);
14 return true;
15 } catch (Exception e) {
16 return false;
17 }
18
19 }
2021 @Override
22 public void waitLock() {
23
24 IZkDataListener zkDataListener = new
IZkDataListener() {
25
26 //节点被改变时触发
27 @Override
28 public void
handleDataChange(String dataPath, Object
data) throws Exception {
29
30 }
31
32 //节点被删除时触发
33 @Override
34 public void
handleDataDeleted(String dataPath) throws
Exception {
35 if (countDownLatch != null){
36
countDownLatch.countDown();
37 }
38 }
39 };
40
41 //注册监听器
42
zkClient.subscribeDataChanges(LOCK_NODE_NAM
E,zkDataListener);
43
44 //如果锁节点存在,阻塞当前线程45 if (zkClient.exists(LOCK_NODE_NAME))
{
46
47 countDownLatch = new
CountDownLatch(1);
48 try {
49 countDownLatch.await();
50
System.out.println(Thread.currentThread().g
etName()+": 等待获取锁");
51 } catch (InterruptedException e)
{
52 }
53 }
54
55 //删除监听
56
zkClient.unsubscribeDataChanges(LOCK_NODE_N
AME,zkDataListener);
57 }
58
59 @Override
60 public void releaseLock() {
61
62 zkClient.delete(LOCK_NODE_NAME);
63 zkClient.close();
64
System.out.println(Thread.currentThread().g
etName()+": 释放锁");
65 }
66
67 }3)创建测试类
1 public class LockTest {
2
3 public static void main(String[] args) {
4
5 //模拟多个10个客户端
6 for (int i=0;i<10;i++) {
7 Thread thread = new Thread(new
LockRunnable());
8 thread.start();
9 }
10 }
11
12 private static class LockRunnable
implements Runnable {
13 @Override
14 public void run() {
15
16 AbstractLock abstractLock = new
LowLock();
17
18 abstractLock.getLock();
19
20 try {
21 TimeUnit.SECONDS.sleep(5);
22 } catch (InterruptedException e)
{
23 e.printStackTrace();
24 }
25
26 abstractLock.releaseLock();
27 }28 }
29 }
4)经过测试可以发现,当一个线程获取到锁之后,其他线程
都会监听这把锁进入到等待状态,一旦持有锁的线程释放锁
后,其他线程则都会监听到,并竞争这把锁。3.4.2.1.2)羊群效应
这种方案的低效点就在于,只有一个锁节点,其他线程都会
监听同一个锁节点,一旦锁节点释放后,其他线程都会收到
通知,然后竞争获取锁节点。这种大量的通知操作会严重降
低zookeeper性能,对于这种由于一个被watch的znode节点
的变化,而造成大量的通知操作,叫做羊群效应。
3.4.2.2)高效锁思想&实现为了避免羊群效应的出现,业界内普遍的解决方案就是,让
获取锁的线程产生排队,后一个监听前一个,依次排序。推
荐使用这种方式实现分布式锁
按照上述流程会在根节点下为每一个等待获取锁的线程创建
一个对应的临时有序节点,序号最小的节点会持有锁,并且
后一个节点只监听其前面的一个节点,从而可以让获取锁的
过程有序且高效。1)定义HighLock类
1 public class HighLock extends AbstractLock{
2
3 private static final String
PARENT_NODE_PATH="/high_lock";
4
5 //当前节点路径
6 private String currentNodePath;
7
8 //前一个节点的路径9 private String preNodePath;
10
11 private CountDownLatch countDownLatch;
12
13 @Override
14 public boolean tryLock() {
15
16 //判断父节点是否存在
17 if
(!zkClient.exists(PARENT_NODE_PATH)){
18 //不存在
19
zkClient.createPersistent(PARENT_NODE_PATH)
;
20 }
21
22 //创建第一个临时有序子节点
23 if (currentNodePath == null ||
"".equals(currentNodePath)){
24
25 //根节点下没有节点信息,将当前节点作为
第一个子节点,类型:临时有序
26 currentNodePath =
zkClient.createEphemeralSequential(PARENT_NO
DE_PATH+"/","lock");
27 }
28
29 //不是第一个子节点,获取父节点下所有子节点
30 List<String> childrenNodeList =
zkClient.getChildren(PARENT_NODE_PATH);
31
32 //子节点升序排序
33 Collections.sort(childrenNodeList);34
35 //判断是否加锁成功
36 if
(currentNodePath.equals(PARENT_NODE_PATH+"/"
+childrenNodeList.get(0))){
37 //当前节点是序号最小的节点
38 return true;
39 }else {
40 //当前节点不是序号最小的节点,获取其前
面的节点名称,并赋值
41 int length =
PARENT_NODE_PATH.length();
42 int currentNodeNumber =
Collections.binarySearch(childrenNodeList,
currentNodePath.substring(length + 1));
43 preNodePath =
PARENT_NODE_PATH+"/"+childrenNodeList.get(cu
rrentNodeNumber-1);
44 }
45 return false;
46 }
47
48 @Override
49 public void waitLock() {
50
51 IZkDataListener zkDataListener = new
IZkDataListener() {
52 @Override
53 public void
handleDataChange(String dataPath, Object
data) throws Exception {
54
55 }56
57 @Override
58 public void
handleDataDeleted(String dataPath) throws
Exception {
59
60 if (countDownLatch != null){
61
countDownLatch.countDown();
62 }
63 }
64 };
65
66 //监听前一个节点的改变
67
zkClient.subscribeDataChanges(preNodePath,z
kDataListener);
68
69 if (zkClient.exists(preNodePath)){
70 countDownLatch = new
CountDownLatch(1);
71
72 try {
73 countDownLatch.await();
74 } catch (InterruptedException e)
{
75
76 }
77 }
78
79
zkClient.unsubscribeDataChanges(preNodePath
,zkDataListener);2)根据结果可以看到,每一个线程都会有自己的节点信息,
并且都会有对应的序号。序号最小的节点首先获取到锁,然
后依次类推。
3.4.2.3)zk锁扣减库存业务实现
}
@Override
public void releaseLock() {
zkClient.delete(currentNodePath);
zkClient.close();
}
}
80
81
82
83
84
85
86
871)生成操作标识是为了防止feign调用超时出现重试,如果
没有操作标识的话,库存服务无法判定是一次操作还是多次
操作,通过标识可以用于区分重试时当前是哪次操作。从而
避免多次扣减库存情况的出现。
2)库存服务先检查redis再检查Mysql,出于两点考虑:
避免服务间重试时,库存服务无法区分是否为同一个操
作,导致相同操作被执行多次。同时缓存结合关系型数据
库,可以起到减轻数据库压力的作用。
库存流水表不仅用于区分操作,同时每一次扣减库存时信
息都会被记录,可以用于后期的库存信息统计等操作。
总的来说,就是通过操作标识结合zookeeper分布式锁,完
成mysql乐观锁的操作,思想上都是相同的。
3.4.2.3.1)实现
1)OrderController修改生成订单方法
/**
* 生成订单
* @param order
* @return
*/
@Idemptent
@PostMapping("/genOrder")
public String genOrder(@RequestBody Order
order) throws InterruptedException {
//新增订单
String orderId =
String.valueOf(idWorker.nextId());
order.setId(orderId);
1
2
3
4
5
6
7
8
9
10
11
1213 order.setCreateTime(new Date());
14 order.setUpdateTime(new Date());
15 orderService.addOrder(order);
16
17 //新增订单明细
18 List<String> goodsIds =
JSON.parseArray(order.getGoodsIds(),
String.class);
19 List<OrderDetail> orderDetailList = new
ArrayList<>();
20
21 for (String goodsId : goodsIds) {
22 OrderDetail orderDetail = new
OrderDetail();
23
orderDetail.setId(String.valueOf(idWorker.n
extId()));
24 orderDetail.setOrderId(orderId);
25 orderDetail.setGoodsId(goodsId);
26 orderDetail.setGoodsPrice(1);
27 orderDetail.setGoodsNum(1);
28
orderDetailService.addOrderDetail(orderDeta
il);
29
30 orderDetailList.add(orderDetail);
31 }
32
33 //生成操作标识并存入redis
34
redisTemplate.opsForValue().set(orderId,"or
derId",30,TimeUnit.MINUTES);
3536 //扣减库存
37
stockFeign.reduceStock(JSON.toJSONString(or
derDetailList),orderId);
38
39 //数据回查
40 if
(stockFlowFeign.findByFlag(orderId).size()
>0){
41 //操作成功
42 return "success";
43 }else {
44 TimeUnit.SECONDS.sleep(3);
45 //异步查询处理结果
46 CompletableFuture<Integer> future1 =
CompletableFuture.supplyAsync(()->{
47 return
stockFlowFeign.findByFlag(orderId).size();
48 },executor);
49 try {
50 if (future1.get()>0){
51 return "success";
52 }
53 }catch (Exception e){
54 throw new RuntimeException("执行
有误");
55 }
56
57 TimeUnit.SECONDS.sleep(5);
58 CompletableFuture<Integer> future2 =
CompletableFuture.supplyAsync(()->{
59 return
stockFlowFeign.findByFlag(orderId).size();60 },executor);
61 try {
62 if (future2.get()>0){
63 return "success";
64 }
65 }catch (Exception e){
66 throw new RuntimeException("执行
有误");
67 }
68
69 TimeUnit.SECONDS.sleep(10);
70 CompletableFuture<Integer> future3 =
CompletableFuture.supplyAsync(()->{
71 return
stockFlowFeign.findByFlag(orderId).size();
72 },executor);
73 try {
74 if (future3.get()>0){
75 return "success";
76 }
77 }catch (Exception e){
78 throw new RuntimeException("执行
有误");
79 }
80 return "false";
81 }
82 }
2)StockController修改扣减库存方法
1 /**
2 * 扣减库存
3 * @param orderListValue4 * @param flag
5 * @throws InterruptedException
6 */
7 @PutMapping("/reduceStock/{flag}")
8 public void reduceStock(@RequestParam String
orderListValue, @PathVariable("flag") String
flag) throws InterruptedException {
9
10 System.out.println("reduce stock");
11
12 //redis验重
13 if (!redisTemplate.delete(flag)){
14 System.out.println("redis验重 重复操
作");
15 return;
16 }
17
18 //MYSQL验重
19 int dbResult =
stockFlowService.findByFlag(flag).size();
20 if (dbResult >0){
21 System.out.println("mysql验重 重复操
作");
22 return;
23 }
24
25 //扣减库存
26 List<OrderDetail> orderDetailList =
JSON.parseArray(orderListValue,
OrderDetail.class);
27
stockService.reduceStock(orderDetailList,fl
ag);28
29 //用于模拟测试服务间重试
30 //TimeUnit.SECONDS.sleep(6);
31
32 }
3)StockServiceImpl中实现扣减库存方法
1 @Autowired
2 private StockMapper stockMapper;
3
4 @Autowired
5 private StockFlowMapper stockFlowMapper;
6
7 @Autowired
8 private IdWorker idWorker;
9
10 @Override
11 @Transactional(rollbackFor =
Exception.class)
12 public Boolean reduceStock(List<OrderDetail>
orderDetailList, String flag) {
13
14 String methodName =
Thread.currentThread().getStackTrace()
[1].getMethodName();
15
16 AbstractLock zkLock = new
HighLock("/"+methodName);
17
18 //加锁
19 try {
2021 zkLock.getLock();
22
23
orderDetailList.stream().forEach(orderDetai
l -> {
24
25 //扣减库存
26 int reduceStockResult =
stockMapper.reduceStock(orderDetail.getGoods
Id(), orderDetail.getGoodsNum());
27 if (reduceStockResult != 1){
28 //扣减库存失败
29 throw new
RuntimeException("扣减库存失败");
30 }
31
32 //新增库存流水
33 StockFlow stockFlow = new
StockFlow();
34
stockFlow.setId(String.valueOf(idWorker.nex
tId()));
35 stockFlow.setFlag(flag);
36
stockFlow.setGoodsId(orderDetail.getGoodsId
());
37
stockFlow.setNum(orderDetail.getGoodsNum())
;
38
stockFlowMapper.insert(stockFlow);
39 });
403.5)redis分布式锁
3.5.1)单节点Redis实现分布式锁
3.5.1.1)原理&实现
分布式锁的一个很重要的特性就是互斥性,同一时间内多个
调用方加锁竞争,只能有一个调用方加锁成功。而redis是基
于单线程模型的,可以利用这个特性让调用方的请求排队,
对于并发请求,只会有一个请求能获取到锁。
redis实现分布式锁也很简单,基于客户端的几个API就可以
完成,主要涉及三个核心API:
setNx():向redis中存key-value,只有当key不存在时才
会设置成功,否则返回0。用于体现互斥性。
expire():设置key的过期时间,用于避免死锁出现。
delete():删除key,用于释放锁。
1)编写工具类实现加锁
通过jedis.set进行加锁,如果返回值是OK,代表加锁成功
return true;
} catch (Exception e) {
e.printStackTrace();
}finally {
//释放锁
zkLock.releaseLock();
}
return false;
}
41
42
43
44
45
46
47
48
49
50如果加锁失败,则自旋不断尝试获取锁,同时在一定时间内
如果仍没有获取到锁,则退出自旋,不再尝试获取锁。
requestId:用于标识当前每个线程自己持有的锁标记
1 public class SingleRedisLock {
2
3 JedisPool jedisPool = new
JedisPool("192.168.200.128",6379);
4
5 //锁过期时间
6 protected long internalLockLeaseTime =
30000;
7
8 //获取锁的超时时间
9 private long timeout = 999999;
10
11 /**
12 * 加锁
13 * @param lockKey 锁键
14 * @param requestId 请求唯一标识
15 * @return
16 */
17 SetParams setParams =
SetParams.setParams().nx().px(internalLockLe
aseTime);
18
19 public boolean tryLock(String lockKey,
String requestId){
20
21 String threadName =
Thread.currentThread().getName();
2223 Jedis jedis =
this.jedisPool.getResource();
24
25 Long start =
System.currentTimeMillis();
26
27 try{
28 for (;;){
29 String lockResult =
jedis.set(lockKey, requestId, setParams);
30 if ("OK".equals(lockResult))
{
31
System.out.println(threadName+": 获取锁成
功");
32 return true;
33 }
34 //否则循环等待,在timeout时间内
仍未获取到锁,则获取失败
35
System.out.println(threadName+": 获取锁失
败,等待中");
36 long l =
System.currentTimeMillis() - start;
37 if (l>=timeout) {
38 return false;
39 }
40 try {
41 Thread.sleep(100);
42 } catch
(InterruptedException e) {
43 e.printStackTrace();
44 }45 }
46 }finally {
47 jedis.close();
48 }
49
50 }
51 }
2)编写工具类实现解锁
解锁时,要避免当前线程将别人的锁释放掉。假设线程A加锁
成功,当过了一段时间线程A来解锁,但线程A的锁已经过期
了,在这个时间节点,线程B也来加锁,因为线程A的锁已经
过期,所以线程B时可以加锁成功的。此时,就会出现问题,
线程A将线程B的锁给释放了。
对于这个问题,就需要使用到加锁时的requestId。当解锁时
要判断当前锁键的value与传入的value是否相同,相同的
话,则代表是同一个人,可以解锁。否则不能解锁。
但是对于这个操作,有非常多的人,会先查询做对比,接着
相同则删除。虽然思路是对的,但是忽略了一个问题,原子
性。判断与删除分成两步执行,则无法保证原子性,一样会
出现问题。所以解锁时不仅要保证加锁和解锁是同一个人还
要保证解锁的原子性。因此结合lua脚本完成查询&删除操
作。
1 /**
2 * 解锁
3 * @param lockKey 锁键
4 * @param requestId 请求唯一标识
5 * @return
6 */7 public boolean releaseLock(String
lockKey,String requestId){
8
9 String threadName =
Thread.currentThread().getName();
10 System.out.println(threadName+":释放
锁");
11 Jedis jedis =
this.jedisPool.getResource();
12
13 String lua =
14 "if redis.call('get',KEYS[1]) ==
ARGV[1] then" +
15 " return redis.call('del',KEYS[1])
" +
16 "else" +
17 " return 0 " +
18 "end";
19
20 try {
21 Object result = jedis.eval(lua,
Collections.singletonList(lockKey),
22
Collections.singletonList(requestId));
23 if("1".equals(result.toString())){
24 return true;
25 }
26 return false;
27 }finally {
28 jedis.close();
29 }
30
31 }3)编写测试类
1 public class LoclTest {
2
3 public static void main(String[] args) {
4
5 //模拟多个5个客户端
6 for (int i=0;i<5;i++) {
7 Thread thread = new Thread(new
LockRunnable());
8 thread.start();
9 }
10 }
11
12 private static class LockRunnable
implements Runnable {
13 @Override
14 public void run() {
15
16 SingleRedisLock singleRedisLock
= new SingleRedisLock();
17
18 String requestId =
UUID.randomUUID().toString();
19 boolean lockResult =
singleRedisLock.tryLock("lock", requestId);
20 if (lockResult){
21
22 try {
23
TimeUnit.SECONDS.sleep(5);
24 } catch
(InterruptedException e) {此时可以发现,多线程会竞争同一把锁,且没有获取获取到
锁的线程会自旋不断尝试去获取锁。每当一个线程将锁释放
后,则会有另外一个线程持有锁。依次类推。
3.5.1.2)单节点问题
锁续期
当对业务进行加锁时,锁的过期时间,绝对不能想当然的设
置一个值。假设线程A在执行某个业务时加锁成功并设置锁过
期时间。但该业务执行时间过长,业务的执行时间超过了锁
过期时间,那么在业务还没执行完时,锁就自动释放了。接
着后续线程就可以获取到锁,又来执行该业务。就会造成线
程A还没执行完,后续线程又来执行,导致同一个业务逻辑被
重复执行。因此对于锁的超时时间,需要结合着业务执行时
间来判断,让锁的过期时间大于业务执行时间。
上面的方案是一个基础解决方案,但是仍然是有问题的。
业务执行时间的影响因素太多了,无法确定一个准确值,只
能是一个估值。无法百分百保证业务执行期间,锁只能被一
个线程占有。
e.printStackTrace();
}
}
singleRedisLock.releaseLock("lock",requestI
d);
}
}
}
25
26
27
28
29
30
31
32如想保证的话,可以在创建锁的同时创建一个守护线程,同
时定义一个定时任务每隔一段时间去为未释放的锁增加过期
时间。当业务执行完,释放锁后,再关闭守护线程。 这种实
现思想可以用来解决锁续期。
服务单点&集群问题
在单点redis虽然可以完成锁操作,可一旦redis服务节点挂掉
了,则无法提供锁操作。
在生产环境下,为了保证redis高可用,会采用异步复制方法
进行主从部署。当主节点写入数据成功,会异步的将数据复
制给从节点,并且当主节点宕机,从节点会被提升为主节点
继续工作。假设主节点写入数据成功,在没有将数据复制给
从节点时,主节点宕机。则会造成提升为主节点的从节点中是没有锁信息的,其他线程则又可以继续加锁,导致互斥失
效。
3.5.2)Redisson实现分布式锁
redisson是redis官网推荐实现分布式锁的一个第三方类库。
其内部完成的功能非常强大,对各种锁都有实现,同时对于
使用者来说非常简单,让使用者能够将更多的关注点放在业
务逻辑上。此处重点利用Redisson解决单机Redis锁产生的两
个问题。
3.5.2.1)单机Redisson实现分布式锁
3.5.2.1.1)实现
基于redisson实现分布式锁很简单,直接基于
lock()&unlock()方法操作即可。
1)添加依赖1 <dependency>
2 <groupId>org.apache.commons</groupId>
3 <artifactId>commons-pool2</artifactId>
4 </dependency>
5 <!--Redis分布式锁-->
6 <dependency>
7 <groupId>org.redisson</groupId>
8 <artifactId>redisson-spring-boot
starter</artifactId>
9 <version>3.13.1</version>
10 </dependency>
2)修改配置文件
1 server:
2 redis:
3 host: 192.168.200.150
4 port: 6379
5 database: 0
6 jedis:
7 pool:
8 max-active: 500
9 max-idle: 1000
10 min-idle: 4
3)修改springboot启动类
1 @Value("${spring.redis.host}")
2 private String host;
3
4 @Value("${spring.redis.port}")
5 private String port;
67 @Bean
8 public RedissonClient redissonClient(){
9 RedissonClient redissonClient;
10
11 Config config = new Config();
12 String url = "redis://" + host + ":" +
port;
13
config.useSingleServer().setAddress(url);
14
15 try {
16 redissonClient =
Redisson.create(config);
17 return redissonClient;
18 } catch (Exception e) {
19 e.printStackTrace();
20 return null;
21 }
22 }
4)定义锁工具类
1 @Component
2 public class RedissonLock {
3
4 @Autowired
5 private RedissonClient redissonClient;
6
7 /**
8 * 加锁
9 * @param lockKey
10 * @return
11 */12 public boolean addLock(String lockKey){
13
14 try {
15 if (redissonClient == null){
16 System.out.println("redisson
client is null");
17 return false;
18 }
19
20 RLock lock =
redissonClient.getLock(lockKey);
21
22 //设置锁超时时间为5秒,到期自动释放
23 lock.lock(10, TimeUnit.SECONDS);
24
25
System.out.println(Thread.currentThread().g
etName()+": 获取到锁");
26
27 //加锁成功
28 return true;
29 } catch (Exception e) {
30 e.printStackTrace();
31 return false;
32 }
33 }
34
35 public boolean releaseLock(String
lockKey){
36
37 try{
38 if (redissonClient == null){39 System.out.println("redisson
client is null");
40 return false;
41 }
42
43 RLock lock =
redissonClient.getLock(lockKey);
44 lock.unlock();
45
System.out.println(Thread.currentThread().g
etName()+": 释放锁");
46 return true;
47 }catch (Exception e){
48 e.printStackTrace();
49 return false;
50 }
51 }
52 }
5)测试
1 @SpringBootTest
2 @RunWith(SpringRunner.class)
3 public class RedissonLockTest {
4
5 @Autowired
6 private RedissonLock redissonLock;
7
8 @Test
9 public void easyLock(){
10 //模拟多个10个客户端
11 for (int i=0;i<10;i++) {12 Thread thread = new Thread(new
LockRunnable());
13 thread.start();
14 }
15
16 try {
17 System.in.read();
18 } catch (IOException e) {
19 e.printStackTrace();
20 }
21 }
22
23 private class LockRunnable implements
Runnable {
24 @Override
25 public void run() {
26 redissonLock.addLock("demo");
27 try {
28 TimeUnit.SECONDS.sleep(3);
29 } catch (InterruptedException e)
{
30 e.printStackTrace();
31 }
32
redissonLock.releaseLock("demo");
33 }
34 }
35 }
6)执行效果根据执行效果可知,多线程并发获取所时,当一个线程获取
到锁,其他线程则获取不到,并且其内部会不断尝试获取
锁,当持有锁的线程将锁释放后,其他线程则会继续去竞争
锁。
3.5.2.1.2)lock()源码分析在上述加锁方法实现中,最核心就是getLock()和lock()。
get()源码非常简单,根据当前传入的锁名称创建并返回一个
RLock对象。
当获取到RLock对象后,调用其内部的lock()执行加锁操作。
根据源码描述,当线程获取锁时,如果没有获取到锁,则会
让其进入自旋,直到获取到锁。 如果获取到锁,则会一直保
留到调用unLock()手动释放或根据传入的leaseTime时间自动
释放。
当前传入两个参数值:锁超时时间,时间单位。主要用于避
免死锁的出现,假设持有锁的redis节点宕机,到期后锁可以
自动释放。lock()方法中还会调用lock()的另外一个重载方法,需要传入
三个参数:过期时间、时间单位、是否中断。
在三个参数的lock()重载方法中,首先会获取当前线程id,接
着调用tryAcquire()方法尝试获取锁,如果返回值为null,代
表获取到锁。 如果返回值不是null,则根据当前线程id创建
异步任务并放入线程池中,接着进入自旋,在自旋过程中,
尝试调用tryAcquire()获取锁,如果获取到则退出自旋。否则
会不断的尝试获取锁。在lock()方法中,最核心的是tryAcquire()。其内部核心实现
会调用tryAcquireAsync(),并传入过期时间、时间单位和当
前线程id,进行锁的获取。如果leaseTime不为-1,代表设置
了有效时间,接着调用tryAcquireAsync()去获取锁。如果
是-1的话,则默认把永不过期改为30秒过期,并且创建异步
任务,如果没有获取到锁,则什么都不做。如果获取到了
锁,则调用scheduleExpirationRenewal()对当前线程id的锁
进行延时。最终的tryLockInnerAsync()则是获取锁的具体实现。可以看
到,其内部是基于lua脚本语言完成锁获取的。因为获取锁的
过程涉及到了多步,为了保证执行过程的原子性,所以使用
了lua,最核心的就是要理解这段lua脚本的执行过程。
对于这款lua脚本来说,KEYS[1]代表需要加锁的key,
ARGV[1]代表锁的超时时间,ARGV[2]代表锁的唯一标识。对
于这段lua脚本,简单来说:
1)检查锁key是否被占用了,如果没有则设置锁key和唯一标
识,初始值为1,并且设置锁key的过期时间。
2)如果锁key存在,并且value也匹配,表示是当前线程持有
的锁,那么重入次数加1,并且设置失效时间。
3)返回锁key的失效时间毫秒数。
3.5.2.1.3)unLock()源码分析
在释放锁时,unlock()内部会调用unlockAsync()对当前线程
持有的锁进行释放。其内部最终会执行unlockInnerAsync()
方法完成锁释放并返回结果。在unlockInnerAsync()中仍然是结合lua脚本完成释放锁操
作。
相关参数:
KEYS[1]:当前锁key。
KEYS[2]:redis消息的ChannelName,每个锁对应唯一
的一个 channelName。
ARGV[1]:redis消息体,用于标记redis的key已经解锁,
用于通知其他线程申请锁。
ARGV[2]:锁超时时间。
ARGV[3]:锁的唯一标识。
1)判断锁key和锁的唯一标识是否匹配,如果不匹配,表示
锁已经被占用,那么直接返回。
2)如果是当前线程持有锁,则value值-1,用于重入操作。
3)如果-1后的值大于0,则对锁设置过期时间。
4)如果-1后的值为0,则删除锁key,并发布消息,该锁已被
释放。用于通知其他线程申请锁。3.5.2.2)锁续期
对于锁续期问题,在单点redis实现分布式锁时已经介绍过
了,用于防止业务执行超时或宕机而引起的业务被重复执
行。
根据对lock方法的解析,可以发现,当设置完过期时间后,
当前锁的过期时间就已经被设定了,不会发生改变,锁到期
后则会被自动释放,因此在业务执行中,通过lock()方法加锁
会造成隐患。
3.5.2.2.1)看门狗
所谓的看门狗是redisson用于自动延长锁有效期的实现机
制。其本质是一个后台线程,用于不断延长锁key的生存时
间。
改造锁示例代码,让锁超时时间为1秒,但是业务执行时,需
要耗时3秒,此时执行可以发现,多线程间在上一个锁没有释
放的情况下,后续线程又获取到了锁。但是解锁的时候,出
现异常,因为加锁时的唯一标识与解锁时的唯一标识发生了
改变,造成死锁。因为业务执行多久无法确定一个准确值,所以在看门狗的实
现中,不需要对锁key设置过期时间,当过期时间为-1时,这
时会启动一个定时任务,在业务释放锁之前,会一直不停的
增加这个锁的有效时间,从而保证在业务执行完毕前,这把
锁不会被提前释放掉。
要开启看门狗机制也很简单,只需要将加锁时使用lock()改为
tryLock()即可。
并且根据之前lock的源码分析,如果没有设置锁超时,默认
过期时间为30秒即watchdog每隔30秒来进行一次续期,该
值可以修改。
进行测试,当加锁后,线程睡眠10秒钟,然后释放锁,可以
看到在这段时间内,当前线程会一直持有锁,直到锁释放。
在多线程环境下,也是阻塞等待进行锁的获取。
3.5.2.2)红锁
当在单点redis中实现redis锁时,一旦redis服务器宕机,则
无法进行锁操作。因此会考虑将redis配置为主从结构,但在
主从结构中,数据复制是异步实现的。假设在主从结构中,
master会异步将数据复制到slave中,一旦某个线程持有了
锁,在还没有将数据复制到slave时,master宕机。则slave
会被提升为master,但被提升为slave的master中并没有之
前线程的锁信息,那么其他线程则又可以重新加锁。
1 config.setLockWatchdogTimeout(3000L);3.5.2.2.1)redlock算法
redlock是一种基于多节点redis实现分布式锁的算法,可以有
效解决redis单点故障的问题。官方建议搭建五台redis服务器
对redlock算法进行实现。
在redis官网中,对于redlock算法的实现思想也做了详细的介
绍。地址:https://redis.io/topics/distlock。整个实现过程
分为五步:
1)记录获取锁前的当前时间
2)使用相同的key,value获取所有redis实例中的锁,并且
设置获取锁的时间要远远小于锁自动释放的时间。假设锁自
动释放时间是10秒,则获取时间应在5-50毫秒之间。通过这
种方式避免客户端长时间等待一个已经关闭的实例,如果一
个实例不可用了,则尝试获取下一个实例。
3)客户端通过获取所有实例的锁后的时间减去第一步的时
间,得到的差值要小于锁自动释放时间,避免拿到一个已经
过期的锁。并且要有超过半数的redis实例成功获取到锁,才
算最终获取锁成功。如果不是超过半数,有可能出现多个客
户端重复获取到锁,导致锁失效。
4)当已经获取到锁,那么它的真正失效时间应该为:过期时
间-第三步的差值。
5)如果客户端获取锁失败,则在所有redis实例中释放掉
锁。为了保证更高效的获取锁,还可以设置重试策略,在一
定时间后重新尝试获取锁,但不能是无休止的,要设置重试
次数。虽然通过redlock能够更加有效的防止redis单点问题,但是仍
然是存在隐患的。假设redis没有开启持久化,clientA获取锁
后,所有redis故障重启,则会导致clientA锁记录消失,
clientB仍然能够获取到锁。这种情况虽然发生几率极低,但
并不能保证肯定不会发生。
保证的方案就是开始AOF持久化,但是要注意同步的策略,
使用每秒同步,如果在一秒内重启,仍然数据丢失。使用
always又会造成性能急剧下降。
官方推荐使用默认的AOF策略即每秒同步,且在redis停掉
后,要在ttl时间后再重启。 缺点就是ttl时间内redis无法对外
提供服务。
3.5.2.2.2)红锁实现
redisson对于红锁的实现已经非常完善,通过其内部提供的
api既可以完成红锁的操作。
1)新建配置类
@Configuration
public class RedissonRedLockConfig {
public RedissonRedLock
initRedissonClient(String lockKey){
Config config1 = new Config();
config1.useSingleServer().setAddress("redis
://192.168.200.150:7000").setDatabase(0);
RedissonClient redissonClient1 =
Redisson.create(config1);
1
2
3
4
5
6
7
89
10 Config config2 = new Config();
11
config2.useSingleServer().setAddress("redis
://192.168.200.150:7001").setDatabase(0);
12 RedissonClient redissonClient2 =
Redisson.create(config2);
13
14 Config config3 = new Config();
15
config3.useSingleServer().setAddress("redis
://192.168.200.150:7002").setDatabase(0);
16 RedissonClient redissonClient3 =
Redisson.create(config3);
17
18 Config config4 = new Config();
19
config4.useSingleServer().setAddress("redis
://192.168.200.150:7003").setDatabase(0);
20 RedissonClient redissonClient4 =
Redisson.create(config4);
21
22 Config config5 = new Config();
23
config5.useSingleServer().setAddress("redis
://192.168.200.150:7004").setDatabase(0);
24 RedissonClient redissonClient5 =
Redisson.create(config5);
25
26 RLock rLock1 =
redissonClient1.getLock(lockKey);
27 RLock rLock2 =
redissonClient2.getLock(lockKey);28 RLock rLock3 =
redissonClient3.getLock(lockKey);
29 RLock rLock4 =
redissonClient4.getLock(lockKey);
30 RLock rLock5 =
redissonClient5.getLock(lockKey);
31
32 RedissonRedLock redissonRedLock =
new
RedissonRedLock(rLock1,rLock2,rLock3,rLock4,
rLock5);
33
34 return redissonRedLock;
35 }
36 }
2)新建测试类,完成加锁与解锁操作
1 @SpringBootTest
2 @RunWith(SpringRunner.class)
3 public class RedLockTest {
4
5 @Autowired
6 private RedissonRedLockConfig
redissonRedLockConfig;
7
8 @Test
9 public void easyLock(){
10 //模拟多个10个客户端
11 for (int i=0;i<10;i++) {
12 Thread thread = new Thread(new
RedLockTest.RedLockRunnable());
13 thread.start();14 }
15
16 try {
17 System.in.read();
18 } catch (IOException e) {
19 e.printStackTrace();
20 }
21 }
22
23 private class RedLockRunnable implements
Runnable {
24 @Override
25 public void run() {
26 RedissonRedLock redissonRedLock
=
redissonRedLockConfig.initRedissonClient("de
mo");
27
28 try {
29 boolean lockResult =
redissonRedLock.tryLock(100, 10,
TimeUnit.SECONDS);
30
31 if (lockResult){
32 System.out.println("获取
锁成功");
33
TimeUnit.SECONDS.sleep(3);
34 }
35 } catch (InterruptedException e)
{
36 e.printStackTrace();
37 }finally {3.5.2.2.3)redissonRedLock加锁源码分析
redissonRedLock.unlock();
System.out.println("释放锁");
}
}
}
}
38
39
40
41
42
43
public boolean tryLock(long waitTime, long
leaseTime, TimeUnit unit) throws
InterruptedException {
long newLeaseTime = -1;
if (leaseTime != -1) {
newLeaseTime =
unit.toMillis(waitTime)*2;
}
long time = System.currentTimeMillis();
long remainTime = -1;
if (waitTime != -1) {
remainTime =
unit.toMillis(waitTime);
}
long lockWaitTime =
calcLockWaitTime(remainTime);
/**
* 1. 允许加锁失败节点个数限制(N-(N/2+1)),
当前假设五个节点,则允许失败节点数为2
*/
int failedLocksLimit =
failedLocksLimit();
/**
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1718 * 2. 遍历所有节点执行lua加锁,用于保证原子性
19 */
20 List<RLock> acquiredLocks = new
ArrayList<>(locks.size());
21 for (ListIterator<RLock> iterator =
locks.listIterator(); iterator.hasNext();)
{
22 RLock lock = iterator.next();
23 boolean lockAcquired;
24 /**
25 * 3.对节点尝试加锁
26 */
27 try {
28 if (waitTime == -1 && leaseTime
== -1) {
29 lockAcquired =
lock.tryLock();
30 } else {
31 long awaitTime =
Math.min(lockWaitTime, remainTime);
32 lockAcquired =
lock.tryLock(awaitTime, newLeaseTime,
TimeUnit.MILLISECONDS);
33 }
34 } catch
(RedisResponseTimeoutException e) {
35 // 如果抛出这类异常,为了防止加锁成
功,但是响应失败,需要解锁所有节点
36
unlockInner(Arrays.asList(lock));
37 lockAcquired = false;
38 } catch (Exception e) {
39 // 抛出异常表示获取锁失败40 lockAcquired = false;
41 }
42
43 if (lockAcquired) {
44 /**
45 *4. 如果获取到锁则添加到已获取锁集
合中
46 */
47 acquiredLocks.add(lock);
48 } else {
49 /**
50 * 5. 计算已经申请锁失败的节点是否已
经到达 允许加锁失败节点个数限制 (N-(N/2+1))
51 * 如果已经到达, 就认定最终申请锁失
败,则没有必要继续从后面的节点申请了
52 * 因为 Redlock 算法要求至少N/2+1
个节点都加锁成功,才算最终的锁申请成功
53 */
54 if (locks.size() -
acquiredLocks.size() == failedLocksLimit())
{
55 break;
56 }
57
58 if (failedLocksLimit == 0) {
59 unlockInner(acquiredLocks);
60 if (waitTime == -1 &&
leaseTime == -1) {
61 return false;
62 }
63 failedLocksLimit =
failedLocksLimit();
64 acquiredLocks.clear();65 // reset iterator
66 while
(iterator.hasPrevious()) {
67 iterator.previous();
68 }
69 } else {
70 failedLocksLimit--;
71 }
72 }
73
74 /**
75 * 6.计算从各个节点获取锁已经消耗的总时
间,如果已经等于最大等待时间,则申请锁失败,返回
false
76 */
77 if (remainTime != -1) {
78 remainTime -=
System.currentTimeMillis() - time;
79 time =
System.currentTimeMillis();
80 if (remainTime <= 0) {
81 unlockInner(acquiredLocks);
82 return false;
83 }
84 }
85 }
86
87 if (leaseTime != -1) {
88 List<RFuture<Boolean>> futures =
new ArrayList<>(acquiredLocks.size());
89 for (RLock rLock : acquiredLocks) {4)消息幂等
在系统中当使用消息队列时,无论做哪种技术选型,有很多
问题是无论如何也不能忽视的,如:消息必达、消息幂等
等。本章节以典型的RabbitMQ为例,讲解如何保证消息幂等
的可实施解决方案,其他MQ选型均可参考。
4.1)消息重试演示
RFuture<Boolean> future =
((RedissonLock)
rLock).expireAsync(unit.toMillis(leaseTime)
, TimeUnit.MILLISECONDS);
futures.add(future);
}
for (RFuture<Boolean> rFuture :
futures) {
rFuture.syncUninterruptibly();
}
}
/**
* 7.如果逻辑正常执行完则认为最终申请锁成功,
返回true
*/
return true;
}
90
91
92
93
94
95
96
97
98
99
100
101
102
103消息队列的消息幂等性,主要是由MQ重试机制引起的。因为
消息生产者将消息发送到MQ-Server后,MQ-Server会将消
息推送到具体的消息消费者。假设由于网络抖动或出现异常
时,MQ-Server根据重试机制就会将消息重新向消息消费者
推送,造成消息消费者多次收到相同消息,造成数据不一
致。
在RabbitMQ中,消息重试机制是默认开启的,但只会在
consumer出现异常时,才会重复推送。在使用中,异常的出
现有可能是由于消费方又去调用第三方接口,由于网络抖动
而造成异常,但是这个异常有可能是暂时的。所以当消费者
出现异常,可以让其重试几次,如果重试几次后,仍然有异
常,则需要进行数据补偿。
数据补偿方案:当重试多次后仍然出现异常,则让此条消息
进入死信队列,最终进入到数据库中,接着设置定时job查询
这些数据,进行手动补偿。
本节中以consumer消费异常为演示主体,因此需要修改
RabbitMQ配置文件。
1)修改consumer一方的配置文件2)设置消费异常
当consumer消息监听类中添加异常,最终接受消息时,可以
发现,消息在接收五次后,最终出现异常。
4.2)消息幂等解决
要保证消息幂等性的话,其实最终要解决的就是保证多次操
作,造成的影响是相同的。那么其解决方案的思路与服务间
幂等的思路其实基本都是一致的。
1)消息防重表,解决思路与服务间幂等的防重表一致。
2)redis。利用redis防重。
这两种方案是最常见的解决方案。其实现思路其实都是一致
的。
# 消费者监听相关配置
listener:
simple:
retry:
# 开启消费者(程序出现异常)重试机制,默认
开启并一直重试
enabled: true
# 最大重试次数
max-attempts: 5
# 重试间隔时间(毫秒)
initial-interval: 3000
1
2
3
4
5
6
7
8
9
104.2.1)修改OrderController
/**
* 此处为了方便演示,不做基础添加数据库操作
* @return
*/
@PostMapping("/addOrder")
public String addOrder(){
String uniqueKey =
String.valueOf(idWorker.nextId());
MessageProperties messageProperties =
new MessageProperties();
messageProperties.setMessageId(uniqueKey);
messageProperties.setContentType("text/plai
n");
messageProperties.setContentEncoding("utf-
8");
1
2
3
4
5
6
7
8
9
10
11
12
134.2.2)修改stockApplication
4.2.3)新增消息监听类
Message message = new
Message("1271700536000909313".getBytes(),mes
sageProperties);
rabbitTemplate.convertAndSend(RabbitMQConfi
g.REDUCE_STOCK_QUEUE,message);
return "success";
}
14
15
16
17
18
@Bean
public JedisPool jedisPool(){
return new
JedisPool("192.168.200.150",6379);
}
1
2
3
4
@Component
public class ReduceStockListener {
@Autowired
private StockService stockService;
@Autowired
private JedisPool jedisPool;
@Autowired
private StockFlowService
stockFlowService;
1
2
3
4
5
6
7
8
9
10
11
1213 @RabbitListener(queues =
RabbitMQConfig.REDUCE_STOCK_QUEUE)
14 @Transactional
15 public void receiveMessage(Message
message){
16
17 //获取消息id
18 String messageId =
message.getMessageProperties().getMessageId(
);
19
20 Jedis jedis =
jedisPool.getResource();
21
22 System.out.println(messageId);
23 try {
24
25 //redis锁去重校验
26 if
(!"OK".equals(jedis.set(messageId,
messageId, "NX", "PX", 300000))){
27 System.out.println("重复请
求");
28 return;
29 }
30
31 //mysql状态校验
32 if (!
(stockFlowService.findByFlag(messageId).size
() == 0)){
33 System.out.println("数据已处
理");
34 return;35 }
36
37 String goodsId = null;
38 try {
39 //获取消息体中goodsId
40 goodsId = new
String(message.getBody(),"utf-8");
41
stockService.reduceStock(goodsId,messageId)
;
42 } catch
(UnsupportedEncodingException e) {
43 e.printStackTrace();
44 }
45
46 int nextInt = new
Random().nextInt(100);
47 System.out.println("随机
数:"+nextInt);
48 if (nextInt%2 ==0){
49 int i= 1/0;
50 }
51
52
53 } catch (RuntimeException e) {
54 //解锁
55 String script = "if
redis.call('get', KEYS[1]) == ARGV[1] then
return redis.call('del', KEYS[1]) else
return 0 end";
56 jedis.eval(script,
Collections.singletonList(messageId),
Collections.singletonList(messageId));4.3)消息缓冲区
对于RabbitMQ的使用,默认情况下,每条消息都会进行分别
的ack通知,消费完一条后,再来消费下一条。但是这样就会
造成大量消息的阻塞情况。所以为了提升消费者对于消息的
消费速度,可以增加consumer数据或者对消息进行批量消
费。MQ接收到producer发送的消息后,不会直接推送给
consumer。而是积攒到一定数量后,再进行消息的发送。
这种方式的实现,可以理解为是一种缓冲区的实现,提升了
消息的消费速度,但是会在一定程度上舍弃结果返回的实时
性。
对于批量消费来说,也是需要考虑幂等的。对于幂等性的解
决方案,沿用刚才的思路即可解决。
System.out.println("出现异常了");
System.out.println(messageId+":
释放锁");
throw e;
}
}
}
57
58
59
60
61
62
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值