开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

Redis应用实战 - 秒杀场景(Nodejs版本)

发表于 2021-07-07

写在前面

公司随着业务量的增加,最近用时几个月时间在项目中全面接入Redis,开发过程中发现市面上缺少具体的实战资料,尤其是在Node.js环境下,能找到的资料要么过于简单入门,要么名不副实,大部分都是属于初级。因此决定把公司这段时间的成果进行分享,会用几篇文章详细介绍Redis的几个使用场景,期望大家一起学习、进步。

下面就开始第一篇,秒杀场景。

业务分析

实际业务中,秒杀包含了许多场景,具体可以分为秒杀前、秒杀中和秒杀后三个阶段,从开发角度具体分析如下:

  1. 秒杀前:主要是做好缓存工作,以应对用户频繁的访问,因为数据是固定的,可以把商品详情页的元素静态化,然后用CDN或者是浏览器进行缓存。
  2. 秒杀中:主要是库存查验,库存扣减和订单处理,这一步的特点是
    • 短时间内大量用户同时进行抢购,系统的流量突然激增,服务器压力瞬间增大(瞬时并发访问高)
    • 请求数量大于商品库存,比如10000个用户抢购,但是库存只有100
    • 限定用户只能在一定时间段内购买
    • 限制单个用户购买数量,避免刷单
    • 抢购是跟数据库打交道,核心功能是下单,库存不能扣成负数
    • 对数据库的操作读多写少,而且读操作相对简单
  3. 秒杀后:主要是一些用户查看已购订单、处理退款和处理物流等等操作,这时候用户请求量已经下降,操作也相对简单,服务器压力不大。

根据上述分析,本文把重点放在秒杀中的开发讲解,其他部分感兴趣的小伙伴可以自己搜索资料,进行尝试。

开发环境

数据库:Redis 3.2.9 + Mysql 5.7.18
服务器:Node.js v10.15.0
测试工具:Jmeter-5.4.1

实战

数据库准备

3439418552-60d4705963d66_fix732.jpeg
如图所示,Mysql中需要创建三张表,分别是

  • 产品表,用于记录产品信息,字段分别为Id、名称、缩略图、价格和状态等等
  • 秒杀活动表,用于记录秒杀活动的详细信息,字段分别为Id、参与秒杀的产品Id、库存量、秒杀开始时间、秒杀结束时间和秒杀活动是否有效等等
  • 订单表,用于记录下单后的数据,字段分别为Id、订单号、产品Id、购买用户Id、订单状态、订单类型和秒杀活动Id等等

下面是创建sql语句,以供参考

1
2
3
4
5
6
7
8
9
10
11
12
sql复制代码CREATE TABLE `seckill_goods` (
`id` INTEGER NOT NULL auto_increment,
`fk_good_id` INTEGER,
`amount` INTEGER,
`start_time` DATETIME,
`end_time` DATETIME,
`is_valid` TINYINT ( 1 ),
`comment` VARCHAR ( 255 ),
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
1
2
3
4
5
6
7
8
9
10
11
12
13
sql复制代码CREATE TABLE `orders` (
`id` INTEGER NOT NULL auto_increment,
`order_no` VARCHAR ( 255 ),
`good_id` INTEGER,
`user_id` INTEGER,
`status` ENUM ( '-1', '0', '1', '2' ),
`order_type` ENUM ( '1', '2' ),
`scekill_id` INTEGER,
`comment` VARCHAR ( 255 ),
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
sql复制代码CREATE TABLE `goods` (
`id` INTEGER NOT NULL auto_increment,
`name` VARCHAR ( 255 ),
`thumbnail` VARCHAR ( 255 ),
`price` INTEGER,
`status` TINYINT ( 1 ),
`stock` INTEGER,
`stock_left` INTEGER,
`description` VARCHAR ( 255 ),
`comment` VARCHAR ( 255 ),
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME NOT NULL,
PRIMARY KEY ( `id` )
) ENGINE = INNODB DEFAULT CHARSET = utf8mb4;

产品表在此次业务中不是重点,以下逻辑都以id=1的产品为示例,请悉知。

秒杀活动表中创建一条库存为200的记录,作为秒杀测试数据,参考下面语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
sql复制代码INSERT INTO `redis_app`.`seckill_goods` (
`id`,
`fk_good_id`,
`amount`,
`start_time`,
`end_time`,
`is_valid`,
`comment`,
`created_at`,
`updated_at`
)
VALUES
(
1,
1,
200,
'2020-06-20 00:00:00',
'2023-06-20 00:00:00',
1,
'...',
'2020-06-20 00:00:00',
'2021-06-22 10:18:16'
);

秒杀接口开发

首先,说一下Node.js中的具体开发环境:

  • web框架使用Koa2
  • mysql操作使用基于promise的Node.js ORM工具Sequelize
  • redis操作使用ioredis库
  • 封装ctx.throwException方法用于处理错误,封装ctx.send方法用于返回正确结果,具体实现参考文末完整代码

其次,分析一下接口要处理的逻辑,大概步骤和顺序如下:

  1. 基本参数校验
  2. 判断产品是否加入了抢购
  3. 判断秒杀活动是否有效
  4. 判断秒杀活动是否开始、结束
  5. 判断秒杀商品是否卖完
  6. 获取登录用户信息
  7. 判断登录用户是否已抢到
  8. 扣库存
  9. 下单

最后,根据分析把以上步骤用代码进行初步实现,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
js复制代码// 引入moment库处理时间相关数据
const moment = require('moment');
// 引入数据库model文件
const seckillModel = require('../../dbs/mysql/models/seckill_goods');
const ordersModel = require('../../dbs/mysql/models/orders');
// 引入工具函数或工具类
const UserModule = require('../modules/user');
const { random_String } = require('../../utils/tools/funcs');

class Seckill {
/**
* 秒杀接口
*
* @method post
* @param good_id 产品id
* @param accessToken 用户Token
* @param path 秒杀完成后跳转路径
*/
async doSeckill(ctx, next) {
const body = ctx.request.body;
const accessToken = ctx.query.accessToken;
const path = body.path;

// 基本参数校验
if (!accessToken || !path) { return ctx.throwException(20001, '参数错误!'); };
// 判断此产品是否加入了抢购
const seckill = await seckillModel.findOne({
where: {
fk_good_id: ctx.params.good_id,
}
});
if (!seckill) { return ctx.throwException(30002, '该产品并未有抢购活动!'); };
// 判断是否有效
if (!seckill.is_valid) { return ctx.throwException(30003, '该活动已结束!'); };
// 判单是否开始、结束
if(moment().isBefore(moment(seckill.start_time))) {
return ctx.throwException(30004, '该抢购活动还未开始!');
}
if(moment().isAfter(moment(seckill.end_time))) {
return ctx.throwException(30005, '该抢购活动已经结束!');
}
// 判断是否卖完
if(seckill.amount < 1) { return ctx.throwException(30006, '该产品已经卖完了!'); };

//获取登录用户信息(这一步只是简单模拟验证用户身份,实际开发中要有严格的accessToken校验流程)
const userInfo = await UserModule.getUserInfo(accessToken);
if (!userInfo) { return ctx.throwException(10002, '用户不存在!'); };

// 判断登录用户是否已抢到(一个用户针对这次活动只能购买一次)
const orderInfo = await ordersModel.findOne({
where: {
user_id: userInfo.id,
seckill_id: seckill.id,
},
});
if (orderInfo) { return ctx.throwException(30007, '该用户已抢到该产品,无需再抢!'); };

// 扣库存
const count = await seckill.decrement('amount');
if (count.amount <= 0) { return ctx.throwException(30006, '该产品已经卖完了!'); };

// 下单
const orderData = {
order_no: Date.now() + random_String(4), // 这里就用当前时间戳加4位随机数作为订单号,实际开发中根据业务规划逻辑
good_id: ctx.params.good_id,
user_id: userInfo.id,
status: '1', // -1 已取消, 0 未付款, 1 已付款, 2已退款
order_type: '2', // 1 常规订单 2 秒杀订单
seckill_id: seckill.id, // 秒杀活动id
comment: '', // 备注
};
const order = ordersModel.create(orderData);

if (!order) { return ctx.throwException(30008, '抢购失败!'); };

ctx.send({
path,
data: '抢购成功!'
});

}

}

module.exports = new Seckill();

至此,秒杀接口用传统的关系型数据库就实现完成了,代码并不复杂,注释也很详细,不用特别的讲解大家也都能看懂,那它能不能正常工作呢,答案显然是否定的

通过Jmeter模拟以下测试:

  • 模拟5000并发下2000个用户进行秒杀,会发现mysql报出timeout错误,同时seckill_goods表amount字段变成负数,orders表中同样产生了多于200的记录(具体数据不同环境下会有差异),这就代表产生了超卖,跟秒杀规则不符
  • 模拟10000并发下单个用户进行秒杀,orders表中产生了多于1条的记录(具体数据不同环境下会有差异),这就说明一个用户针对这次活动买了多次,跟秒杀规则不符

分析下代码会发现这其中的问题:

  • 步骤2,判断此产品是否加入了抢购

直接在mysql中查询,因为是在秒杀场景下,并发会很高,大量的请求到数据库,显然mysql是扛不住的,毕竟mysql每秒只能支撑千级别的并发请求

  • 步骤7,判断登录用户是否已抢到

在高并发下同一个用户上个订单还没有生成成功,再次判断是否抢到依然会判断为否,这种情况下代码并没有对扣减和下单操作做任何限制,因此就产生了单个用户购买多个产品的情况,跟一个用户针对这次活动只能购买一次的要求不符

  • 步骤8,扣库存操作

假设同时有1000个请求,这1000个请求在步骤5判断产品是否秒杀完的时候查询到的库存都是200,因此这些请求都会执行步骤8扣减库存,那库存肯定会变成负数,也就是产生了超卖现象

解决方案

经过分析得到三个问题需要解决:

  1. 秒杀数据需要支持高并发访问
  2. 一个用户针对这次活动只能购买一次的问题,也就是限购问题
  3. 减库存不能扣成负数,订单数不能超过设置的库存数,也就是超卖问题

Redis作为内存型数据库,本身高速处理请求的特性可以支持高并发。针对超卖,也就是库存扣减变负数情况,Redis可以提供Lua脚本保证原子性和分布式锁两个解决高并发下数据不一致的问题。针对一个用户只能购买一次的要求,Redis的分布式锁可以解决问题。

因此,可以尝试用Redis解决上述问题,具体操作:

  • 为了支撑大量高并发的库存查验请求,需要用Redis保存秒杀活动数据(即seckill_goods表数据),这样一来请求可以直接从Redis中读取库存并进行查询,完成查询之后如果还有库存余量,就直接从Redis中扣除库存
  • 扣减库存操作在Redis中进行,但是因为Redis扣减这一操作是分为读和写两个步骤,也就是必须先读数据进行判断再执行减操作,因此如果对这两个操作没有做好控制,就导致数据被改错,依然会出现超卖现象,为了保证并发访问的正确性需要使用原子操作解决问题,Redis提供了使用Lua脚本包含多个操作来实现原子性的方案

以下是Redis官方文档对Lua脚本原子性的解释

Atomicity of scripts
Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one of MULTI / EXEC. From the point of view of all the other clients the effects of a script are either still not visible or already completed.

  • 使用Redis实现分布式锁,对扣库存和写订单操作进行加锁,以保证一个用户只能购买一次的问题。

接入Redis

首先,不再使用seckill_goods表,新增秒杀活动逻辑变为在Redis中插入数据,类型为hash类型,key规则为seckill_good_ + 产品id,现在假设新增一条key为seckill_good_1的记录,值为

1
2
3
4
5
6
7
js复制代码{
amount: 200,
start_time: '2020-06-20 00:00:00',
end_time: '2023-06-20 00:00:00',
is_valid: 1,
comment: '...',
}

其次,创建lua脚本保证扣减操作的原子性,脚本内容如下

1
2
3
4
5
6
7
8
lua复制代码if (redis.call('hexists', KEYS[1], KEYS[2]) == 1) then
local stock = tonumber(redis.call('hget', KEYS[1], KEYS[2]));
if (stock > 0) then
redis.call('hincrby', KEYS[1], KEYS[2], -1);
return stock
end;
return 0
end;

最后,完成代码,完整代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
js复制代码// 引入相关库
const moment = require('moment');
const Op = require('sequelize').Op;
const { v4: uuidv4 } = require('uuid');
// 引入数据库model文件
const seckillModel = require('../../dbs/mysql/models/seckill_goods');
const ordersModel = require('../../dbs/mysql/models/orders');
// 引入Redis实例
const redis = require('../../dbs/redis');
// 引入工具函数或工具类
const UserModule = require('../modules/user');
const { randomString, checkObjNull } = require('../../utils/tools/funcs');
// 引入秒杀key前缀
const { SECKILL_GOOD, LOCK_KEY } = require('../../utils/constants/redis-prefixs');
// 引入避免超卖lua脚本
const { stock, lock, unlock } = require('../../utils/scripts');

class Seckill {
async doSeckill(ctx, next) {
const body = ctx.request.body;
const goodId = ctx.params.good_id;
const accessToken = ctx.query.accessToken;
const path = body.path;

// 基本参数校验
if (!accessToken || !path) { return ctx.throwException(20001, '参数错误!'); };
// 判断此产品是否加入了抢购
const key = `${SECKILL_GOOD}${goodId}`;
const seckill = await redis.hgetall(key);
if (!checkObjNull(seckill)) { return ctx.throwException(30002, '该产品并未有抢购活动!'); };
// 判断是否有效
if (!seckill.is_valid) { return ctx.throwException(30003, '该活动已结束!'); };
// 判单是否开始、结束
if(moment().isBefore(moment(seckill.start_time))) {
return ctx.throwException(30004, '该抢购活动还未开始!');
}
if(moment().isAfter(moment(seckill.end_time))) {
return ctx.throwException(30005, '该抢购活动已经结束!');
}
// 判断是否卖完
if(seckill.amount < 1) { return ctx.throwException(30006, '该产品已经卖完了!'); };

//获取登录用户信息(这一步只是简单模拟验证用户身份,实际开发中要有严格的登录注册校验流程)
const userInfo = await UserModule.getUserInfo(accessToken);
if (!userInfo) { return ctx.throwException(10002, '用户不存在!'); };

// 判断登录用户是否已抢到
const orderInfo = await ordersModel.findOne({
where: {
user_id: userInfo.id,
good_id: goodId,
status: { [Op.between]: ['0', '1'] },
},
});
if (orderInfo) { return ctx.throwException(30007, '该用户已抢到该产品,无需再抢!'); };

// 加锁,实现一个用户针对这次活动只能购买一次
const lockKey = `${LOCK_KEY}${userInfo.id}:${goodId}`; // 锁的key有用户id和商品id组成
const uuid = uuidv4();
const expireTime = moment(seckill.end_time).diff(moment(), 'minutes'); // 锁存在时间为当前时间和活动结束的时间差
const tryLock = await redis.eval(lock, 2, [lockKey, 'releaseTime', uuid, expireTime]);

try {
if (tryLock === 1) {
// 扣库存
const count = await redis.eval(stock, 2, [key, 'amount', '', '']);
if (count <= 0) { return ctx.throwException(30006, '该产品已经卖完了!'); };

// 下单
const orderData = {
order_no: Date.now() + randomString(4), // 这里就用当前时间戳加4位随机数作为订单号,实际开发中根据业务规划逻辑
good_id: goodId,
user_id: userInfo.id,
status: '1', // -1 已取消, 0 未付款, 1 已付款, 2已退款
order_type: '2', // 1 常规订单 2 秒杀订单
// seckill_id: seckill.id, // 秒杀活动id, redis中不维护秒杀活动id
comment: '', // 备注
};
const order = ordersModel.create(orderData);

if (!order) { return ctx.throwException(30008, '抢购失败!'); };
}
} catch (e) {
await redis.eval(unlock, 1, [lockKey, uuid]);
return ctx.throwException(30006, '该产品已经卖完了!');
}

ctx.send({
path,
data: '抢购成功!'
});
}

}

module.exports = new Seckill();

这里代码主要做个四个修改:

  1. 步骤2,判断产品是否加入了抢购,改为去Redis中查询
  2. 步骤7,判断登录用户是否已抢到,因为不在维护抢购活动id,所以改为使用用户id、产品id和状态status判断
  3. 步骤8,扣库存,改为使用lua脚本去Redis中扣库存
  4. 对扣库存和写入数据库操作进行加锁

订单的操作仍然在Mysql数据库中进行,因为大部分的请求都在步骤5被拦截了,剩余请求Mysql是完全有能力处理的。

再次通过Jmeter进行测试,发现订单表正常,库存量扣减正常,说明超卖问题和限购已经解决。

其他问题

  1. 秒杀场景的其他技术
    基于Redis支持高并发、键值对型数据库和支持原子操作等特点,案例中使用Redis来作为秒杀应对方案。在更复杂的秒杀场景下,除了使用Redis外,在必要的的情况下还需要用到其他一些技术:
* 限流,用漏斗算法、令牌桶算法等进行限流
* 缓存,把热点数据缓存到内存里,尽可能缓解数据库访问的压力
* 削峰,使用消息队列和缓存技术使瞬间高流量转变成一段时间的平稳流量,比如客户抢购成功后,立即返回响应,然后通过消息队列异步处理后续步骤,发短信,写日志,更新一致性低的数据库等等
* 异步,假设商家创建一个只针对粉丝的秒杀活动,如果商家的粉丝比较少(假设小于1000),那么秒杀活动直接推送给所有粉丝,如果用户粉丝比较多,程序立刻推送给排名前1000的用户,其余用户采用消息队列延迟推送。(1000这个数字需要根据具体情况决定,比如粉丝数2000以内的商家占99%,只有1%的用户粉丝超过2000,那么这个值就应该设置为2000)
* 分流,单台服务器不行就上集群,通过负载均衡共同去处理请求,分散压力这些技术的应用会让整个秒杀系统更加完善,但是核心技术还是`Redis`,可以说用好`Redis`实现的秒杀系统就足以应对大部分场景。
  1. Redis健壮性
    案例使用的是单机版Redis,单节点在生产环境基本上不会使用,因为
* 不能达到高可用
* 即便有着`AOF`日志和`RDB`快照的解决方案以保证数据不丢失,但都只能放在`master`上,一旦机器故障,服务就无法运行,而且即便采取了相应措施仍不可避免的会造成数据丢失。因此,`Redis`的主从机制和集群机制在生产环境下是必须的。
  1. Redis分布式锁的问题
* 单点分布式锁,案例提到的分布式锁,实际上更准确的说法是单点分布式锁,是为了方便演示,但是,单点`Redis`分布式锁是肯定不能用在生产环境的,理由跟第2点类似
* 以主从机制(多机器)为基础的分布式锁,也是不够的,因为`redis`在进行主从复制时是异步完成的,比如在`clientA`获取锁后,主`redis`复制数据到从`redis`过程中崩溃了,导致锁没有复制到从`redis`中,然后从`redis`选举出一个升级为主`redis`,造成新的主`redis`没有`clientA`设置的锁,这时`clientB`尝试获取锁,并且能够成功获取锁,导致互斥失效。针对以上问题,`redis`官方设计了`Redlock`,在`Node.js`环境下对应的资源库为`node-redlock`,可以用`npm`安装,至少需要3个独立的服务器或集群才能使用,提供了非常高的容错率,在生产环境中应该优先采用此方案部署。

总结

秒杀场景的特点可以总结为瞬时并发访问、读多写少、限时和限量,开发中还要考虑避免超卖现象以及类似黄牛抢票的限购问题,针对以上特点和问题,分析得到开发的原则是:数据写入内存而不是写入硬盘,异步处理而不是同步处理,扣库存操作原子执行以及对单用户购买进行加锁,而Redis正好是符合以上全部特点的工具,因此最终选择Redis来解决问题。

秒杀场景是一个在电商业务中相对复杂的场景,此篇文章只是介绍了其中最核心的逻辑,实际业务可能更加复杂,但只需要在此核心基础上进行扩展和优化即可。

秒杀场景的解决方案不仅仅适合秒杀,类似的还有抢红包、抢优惠券以及抢票等等,思路都是一致的。

解决方案的思路还可以应用在单独限购、第二件半价以及控制库存等等诸多场景,大家要灵活运用。

项目地址

github.com/threerocks/…

参考资料

time.geekbang.org/column/arti…
redis.io/topics/dist…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

线程池系列之CallerRunsPolicy()拒绝策略 1

发表于 2021-07-07

1.背景

学习线程池相关知识时,我们都知道线程池的拒绝策略有四种,其中有一种为CallerRunsPolicy()策略,查阅过很多知识,说法不一,因此本文通过实际代码测试详解CallerRunsPolicy()拒绝策略

2.CallerRunsPolicy拒绝策略的定义

CallerRunsPolicy类的注释是这样描述的

A handler for rejected tasks that runs the rejected task directly in the calling thread of the execute method, unless the executor has been shut down, in which case the task is discarded.

简单翻译过来就是对于被拒绝的任务,由调用 execute method方法的线程来执行

怎么理解呢?大家看下面代码

1.创建了一个线程,并且打印出了线程的名字

2.线程中定义了核心线程数为2的线程池,一共有7个任务要执行,其中2个任务创建线程执行去了,3个任务放入了任务队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码import java.util.concurrent.*;

/**
* @author linsanity
* @date 2021-07-07 17:21
* @desc
*/
class MyTask implements Runnable {
private String id;

public MyTask(String id) {
this.id = id;
}

public void run() {
System.out.println(id+"线程名"+Thread.currentThread().getName());
}
}

public class RejectPolicy {
public static void main(String[] args) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
ExecutorService es = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<Runnable>(3), Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
MyTask t1 = new MyTask("id:1");
MyTask t2 = new MyTask("id:2");
MyTask t3 = new MyTask("id:3");
MyTask t4 = new MyTask("id:4");
MyTask t5 = new MyTask("id:5");
MyTask t6 = new MyTask("id:6");
MyTask t7 = new MyTask("id:7");

es.execute(t1);
es.execute(t2);
es.execute(t3);
es.execute(t4);
es.execute(t5);
es.execute(t6);
es.execute(t7);
}
});

System.out.println("new Thread得到的线程名:"+thread.getName());
thread.start();
}
}

输出如下

1
2
3
4
5
6
7
8
vbnet复制代码new Thread得到的线程名:Thread-0
id:6线程名Thread-0
id:7线程名Thread-0
id:1线程名pool-1-thread-1
id:3线程名pool-1-thread-1
id:2线程名pool-1-thread-2
id:4线程名pool-1-thread-1
id:5线程名pool-1-thread-2

可以看到任务6和任务7的执行线程并非是线程池中的线程执行的

3.结论

CallerRunsPolicy()拒绝策略是指被拒绝的任务,由[调用execute方法的线程]来执行被拒绝的任务(哪个线程调用了execute方法,那么这个线程来执行被拒绝的任务)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis的SETNX的使用

发表于 2021-07-07

一.介绍

在 Redis 里,所谓 SETNX,是「SET if Not eXists」的缩写,也就是只有不存在的时候才设置,可以利用它来实现锁的效果。

SETNX key value

将 key 的值设为 value ,当且仅当 key 不存在。
若给定的 key 已经存在,则 SETNX 不做任何动作。

二.选项命令

在SET命令中,有很多选项可用来修改命令的行为。 以下是SET命令可用选项的基本语法。

redis 127.0.0.1:6379> SET KEY VALUE [EX seconds] [PX milliseconds] [NX|XX]

(1)设置指定的到期时间(以秒为单位)。

ShellEX seconds −

(2)设置指定的到期时间(以毫秒为单位)

PX milliseconds

(3)仅在键不存在时设置键

NX

(4)只有在键已存在时才设置

XX

示例

redis 127.0.0.1:6379> SET mykey “redis” EX 60 NX
OK

Shell以上示例将在键“mykey”不存在时,设置键的值,到期时间为60秒。

三.使用redisTemplate操作SetNx

1.设置setNx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码  /**
* @Author lss0555
* @Description setNx操作
**/
@Override
public boolean setNx(String key,String value, long time) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.set(key, value, "NX", "PX", time);
};
String result = redisTemplate.execute(callback);

return !StringUtils.isEmpty(result);
} catch (Exception e) {
logger.error("set redis occured an exception", e);
}
return false;
}
2.获取getNx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
* @Description getNx
**/
@Override
public String getNx(String key) {
try {
RedisCallback<String> callback = (connection) -> {
JedisCommands commands = (JedisCommands) connection.getNativeConnection();
return commands.get(key);
};
String result = redisTemplate.execute(callback);
return result;
} catch (Exception e) {
logger.error("get redis occured an exception", e);
}
return "";
}
3.lua脚本删除redis中匹配value的key

在分布式锁的应用中,使用lua脚本删除redis中匹配value的key,可以避免由于方法执行时间过长而redis锁自动过期失效的时候误删其他线程的锁,spring自带的执行脚本方法中,集群模式直接抛出不支持执行脚本的异常,所以只能拿到原redis的connection来执行脚本。

1.使用脚本的好处
(1)减少网络开销,在Lua脚本中可以把多个命令放在同一个脚本中运行
(2)原子操作,redis会将整个脚本作为一个整体执行,中间不会被其他命令插入。
(3)复用性,客户端发送的脚本会永远存储在redis中,这意味着其他客户端可以复用这一脚本来完成同样的逻辑

2.使用示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
java复制代码@Override
public boolean unlcok(String key,String value) {
try {
List<String> keys = new ArrayList<>();
keys.add(key);
List<String> args = new ArrayList<>();
args.add(value);
RedisCallback<Long> callback = (connection) -> {
Object nativeConnection = connection.getNativeConnection();
// 集群模式和单机模式虽然执行脚本的方法一样,但是没有共同的接口,所以只能分开执行
// 集群模式
if (nativeConnection instanceof JedisCluster) {
return (Long) ((JedisCluster) nativeConnection).eval(UNLOCK_LUA, keys, args);
}

// 单机模式
else if (nativeConnection instanceof Jedis) {
return (Long) ((Jedis) nativeConnection).eval(UNLOCK_LUA, keys, args);
}
return 0L;
};
Long result = redisTemplate.execute(callback);
return result != null && result > 0;
} catch (Exception e) {
logger.error("release lock occured an exception", e);
} finally {
// 清除掉ThreadLocal中的数据,避免内存溢出
//lockFlag.remove();
}
return false;
}

public static final String UNLOCK_LUA;
static {
StringBuilder sb = new StringBuilder();
sb.append("if redis.call(\"get\",KEYS[1]) == ARGV[1] ");
sb.append("then ");
sb.append(" return redis.call(\"del\",KEYS[1]) ");
sb.append("else ");
sb.append(" return 0 ");
sb.append("end ");
UNLOCK_LUA = sb.toString();
}

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【2021最新升级秘籍】手把手教你如何快速安全的升级Orac

发表于 2021-07-07

「本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!」

作者简介

  • 作者:LuciferLiu,中国DBA联盟(ACDU)成员。
  • 目前从事Oracle DBA工作,曾从事 Oracle 数据库开发工作,主要服务于生产制造,汽车金融等行业。
  • 现拥有Oracle OCP,OceanBase OBCA认证,擅长Oracle数据库运维开发,备份恢复,安装迁移,Linux自动化运维脚本编写等。

前言

Oralce 19C版本已经趋于成熟,而11GR2版本Oracle已经在20年停止支持,意味着不再更新bug补丁。因此,升级19C是未来的大趋势,本文就来讲解下Oracle如何快速安装的升级到19C版本。

升级流程

一、环境准备

演示环境安装过程忽略,可参考:

  • 10分钟!一键部署Oracle 11GR2单机
  • 30分钟!一键部署Oracle 19C单机CDB+PDB

脚本下载 Github:github.com/pc-study/In…

本次测试尽量按照生产环境升级进行模拟,故而使用2台主机进行测试:

节点 主机版本 主机名 实例名 Oracle版本 IP地址
源库 RHEL6.9 lucifer lucifer 11204(无补丁) 10.211.55.110
目标库 RHEL6.9 cdb10c 11204(补丁:29585399) 10.211.55.102

注意:源库为生产环境linux 6系统,目标库为升级环境,由于19C无法安装在linux 6系统,故而选择异机升级,保留生产环境用于失败回退。

根据 MOS文档 2485457.1 可以获取最新版AutoUpgrade工具下载地址:

  • The most recent version of AutoUpgrade can be downloaded via this link: version 20210421.

二、升级前准备

拷贝19C的jdk到源库:

1
bash复制代码 scp -r $ORACLE_HOME/jdk/ 10.211.55.110:/soft/

注意:AutoUpgrade工具需要JDK版本1.8以上,11GR2的jdk版本为1.5不支持,因此需要使用19C的ORACLE_HOME中JDK版本。

1 设置JAVA环境变量

  • oracle用户下java环境变量配置
1
2
3
4
5
6
7
8
9
10
11
bash复制代码##使用19c环境的ORACLE_HOME JDK
su - oracle
cat<<EOF >>/home/oracle/.bash_profile
export JAVA_HOME=/soft/jdk/bin
export PATH=/soft/jdk/bin:\$PATH
EOF

source /home/oracle/.bash_profile

java -version
java -jar /soft/autoupgrade.jar -version

2 源端创建并编辑config文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
bash复制代码java -jar /soft/autoupgrade.jar -create_sample_file config /soft/config.cfg

##参照生成的config文件,编写config
mkdir /soft/upg_logs /soft/logs
cat<<EOF >/soft/config.cfg
global.autoupg_log_dir=/soft/upg_logs
#
# Database number 1
#
upg1.dbname=lucifer
upg1.start_time=NOW
upg1.source_home=/u01/app/oracle/product/11.2.0/db
upg1.target_home=/u01/app/oracle/product/19.3.0/db
upg1.sid=lucifer
upg1.log_dir=/soft/logs
upg1.upgrade_node=localhost
upg1.target_version=19
upg1.restoration=no
EOF

chown -R oracle:oinstall /soft

3 升级前源库进行分析检查

1
bash复制代码java -jar /soft/autoupgrade.jar -config /soft/config.cfg -mode analyze

注意:可以通过 lsj 命令查看当前JOB的运行情况。

  • 可以通过网页查看检查情况:
1
2
bash复制代码cd /soft/logs
python -m SimpleHTTPServer 8000

打开网页访问 http://10.211.55.110:8000/lucifer/100/prechecks/lucifer_preupgrade.html

4 升级前源库执行修复脚本

1
bash复制代码java -jar /soft/autoupgrade.jar -config /soft/config.cfg -mode fixups

注意:可以通过 status -job 101 命令查看当前JOB的运行情况。

三、正式升级

1 关闭源库

1
2
bash复制代码sqlplus / as sysdba
shutdown immediate

2 拷贝源库数据文件,日志文件,参数文件,密码文件到目标端,均在源端操作

1
2
3
4
5
6
7
8
bash复制代码su - oracle
##拷贝数据文件,控制文件,日志文件,临时文件
scp -r /oradata/lucifer/ 10.211.55.102:/oradata
scp -r /u01/app/oracle/fast_recovery_area/lucifer/control02.ctl 10.211.55.102:/oradata/lucifer
##拷贝参数文件
scp spfilelucifer.ora 10.211.55.102:/u01/app/oracle/product/19.3.0/db/dbs
##拷贝密码文件
scp orapwlucifer 10.211.55.102:/u01/app/oracle/product/19.3.0/db/dbs

3 目标库打开实例到upgrade模式,均在目标端操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bash复制代码##创建文件夹
mkdir -p /u01/app/oracle/admin/lucifer/adump
mkdir -p /u01/app/oracle/fast_recovery_area/lucifer

mv /oradata/lucifer/control02.ctl /u01/app/oracle/fast_recovery_area/lucifer/control02.ctl

##/etc/oratab增加oracle_sid
cat <<EOF >>/etc/oratab
lucifer:/u01/app/oracle/product/19.3.0/db:Y
EOF

su - oracle
##替换环境变量或者设置ORACLE_SID
export ORACLE_SID=lucifer
sqlplus / as sysdba
startup upgrade
  • Oracle环境变量如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
bash复制代码################OracleBegin#########################
umask 022
export TMP=/tmp
export TMPDIR=$TMP
export NLS_LANG=AMERICAN_AMERICA.AL32UTF8 #AL32UTF8,ZHS16GBK
export ORACLE_BASE=/u01/app/oracle
export ORACLE_HOME=/u01/app/oracle/product/19.3.0/db
export ORACLE_HOSTNAME=cdb19c
export ORACLE_TERM=xterm
export TNS_ADMIN=$ORACLE_HOME/network/admin
export LD_LIBRARY_PATH=$ORACLE_HOME/lib:/lib:/usr/lib
export ORACLE_SID=lucifer
export PATH=/usr/sbin:$PATH
export PATH=$ORACLE_HOME/bin:$ORACLE_HOME/OPatch:$PATH
alias sas='sqlplus / as sysdba'
alias alert='tail -500f $ORACLE_BASE/diag/rdbms/$ORACLE_SID/$ORACLE_SID/trace/alert_$ORACLE_SID.log|more'
export PS1="[`whoami`@`hostname`:"'$PWD]$ '
alias sqlplus='rlwrap sqlplus'
alias rman='rlwrap rman'
alias lsnrctl='rlwrap lsnrctl'
alias asmcmd='rlwrap asmcmd'
alias adrci='rlwrap adrci'
alias ggsci='rlwrap ggsci'
alias dgmgrl='rlwrap dgmgrl'
################OracleEnd###########################
export JAVA_HOME=$ORACLE_HOME/jdk/bin
export PATH=$ORACLE_HOME/jdk/bin:$PATH

4 目标端创建并编辑config文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
bash复制代码java -jar /soft/autoupgrade.jar -create_sample_file config /soft/config.cfg

##参照生成的config文件,编写config
mkdir /soft/upg_logs /soft/logs
cat<<EOF >/soft/config.cfg
global.autoupg_log_dir=/soft/upg_logs
upg1.dbname=lucifer
upg1.start_time=NOW
upg1.source_home=/tmp
upg1.target_home=/u01/app/oracle/product/19.3.0/db
upg1.sid=lucifer
upg1.log_dir=/soft/logs
upg1.upgrade_node=localhost
upg1.target_version=19
upg1.restoration=no
EOF

chown -R oracle:oinstall /soft

注意:源端目录可以随意填写一个目录,例如:/tmp。

5 目标端执行升级操作(upgrade模式)

1
bash复制代码java -jar /soft/autoupgrade.jar -config /soft/config.cfg -mode upgrade

6 监控升级情况

  • 通过python来创建一个HTTPServer网页来监控升级情况:
1
2
bash复制代码cd /soft/upg_logs/cfgtoollogs/upgrade/auto
python -m SimpleHTTPServer 8000
  • 打开网页访问:http://10.211.55.102:8000/state.html,网页会自动刷新执行情况:

  • 等待升级完成即可:

至此,AutoUpgrade工具升级结束。

四、升级后处理

1 配置sqlnet.ora

1
2
3
4
5
bash复制代码cd $TNS_ADMIN
cat <<EOF >>sqlnet.ora
SQLNET.ALLOWED_LOGON_VERSION_CLIENT=8
SQLNET.ALLOWED_LOGON_VERSION_SERVER=8
EOF

2 检查所有组件

1
2
3
bash复制代码select substr(comp_id,1,15) comp_id,substr(comp_name,1,30) comp_name,substr(version,1,10) version,status
from dba_registry
order by modified;

五、升级为PDB并且插入CDB

  • 通过以上操作可以异机升级数据库,但是只升级到NON-CDB模式。那么如何直接升级成PDB呢?

1 目标端需要创建CDB模式的数据库实例

2 使用刚刚升级成功的lucifer作为源端进行转pdb

  • 目标端创建并编辑config文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
bash复制代码java -jar /soft/autoupgrade.jar -create_sample_file config /soft/config.cfg

##参照生成的config文件,编写config
mkdir /soft/upg_logs /soft/logs
rm -rf /soft/upg_logs/*
rm -rf /soft/logs/*

cat<<EOF >/soft/config.cfg
global.autoupg_log_dir=/soft/upg_logs
upg1.dbname=lucifer
upg1.start_time=NOW
upg1.source_home=/u01/app/oracle/product/19.3.0/db
upg1.target_home=/u01/app/oracle/product/19.3.0/db
upg1.sid=lucifer
upg1.log_dir=/soft/logs
upg1.upgrade_node=localhost
upg1.target_version=19
upg1.restoration=no
upg1.target_cdb=cdb19c
upg1.target_pdb_name=lucifer
upg3.target_pdb_copy_option=file_name_convert=('/oradata/lucifer/', '/oradata/CDB19C/lucifer/')
EOF

chown -R oracle:oinstall /soft

  • 目标端执行升级操作(deploy模式)
1
bash复制代码java -jar /soft/autoupgrade.jar -config /soft/config.cfg -mode deploy

  • 等待转换完毕

  • 升级后检查

)

至此,完整的升级流程已经演示结束,希望能够帮助到。

参考文档:

  • Oracle AutoUpgrade between two servers
  • Oracle AutoUpgrade between two servers – and Plugin?
  • AutoUpgrade with Source and Target Database Homes on Different Servers

本次分享到此结束啦~

如果觉得文章对你有帮助,点赞、收藏、关注、评论,一键四连支持,你的支持就是我创作最大的动力。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Boot demo系列(十四):Shardin

发表于 2021-07-07

1 概述

之前笔者写过两篇文章:

  • ShardingSphere 读写分离
  • ShardingSphere 分库分表

这里将两者结合起来,实现读写分离+分库分表的功能。关于环境的配置本文将进行简化叙述,详细可以参考前两篇文章。

2 环境

  • MySQL 8.0.25(Docker)
  • MyBatis Plus 3.4.3.1
  • MyBatis Plus Generator 3.5.0
  • Druid 1.2.6
  • ShardingSphere 4.1.1
  • Yitter 1.0.6(一个雪花id生成器)

3 数据库环境准备

由于环境准备不是本文的重点,一主一从的主从复制环境可以参考此处搭建。

准备好环境,本地启动两个MySQL,主节点环境:

  • 名字:master
  • 端口:3306
  • 数据库:两个库(test0、test1)
  • 数据表:六个表,每个库三个(test0.user0、test0.user1、test0.user2、test1.user0、test1.user1、test1.user2)

从节点环境:

  • 名字:slave
  • 端口:3307
  • 数据库:两个库(test0、test1)
  • 数据表:六个表,每个库三个(test0.user0、test0.user1、test0.user2、test1.user0、test1.user1、test1.user2)

主库配置文件:

1
2
3
4
bash复制代码[mysqld]
server-id=1
binlog-do-db=test0
binlog-do-db=test1

从库配置文件:

1
2
3
4
bash复制代码[mysqld]
server-id=2
replicate-do-db=test0
replicate-do-db=test1

完整的数据库脚本和MySQL配置文件放在文末的源码链接中。

4 新建项目

新建项目并引入如下依赖:

  • Druid
  • MyBatis Plus starter
  • MyBaits Plus Generator
  • Velocity core
  • ShardingSphere
  • Yitter

Maven如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
xml复制代码<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.0</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.3</version>
</dependency>
<dependency>
<groupId>org.realityforge.org.jetbrains.annotations</groupId>
<artifactId>org.jetbrains.annotations</artifactId>
<version>1.7.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.6</version>
</dependency>
<dependency>
<groupId>org.apache.shardingsphere</groupId>
<artifactId>sharding-jdbc-spring-boot-starter</artifactId>
<version>4.1.1</version>
</dependency>
<dependency>
<groupId>com.github.yitter</groupId>
<artifactId>yitter-idgenerator</artifactId>
<version>1.0.6</version>
</dependency>

Gradle如下:

1
2
3
4
5
6
bash复制代码implementation 'com.baomidou:mybatis-plus-boot-starter:3.4.3.1'
implementation 'org.apache.velocity:velocity-engine-core:2.3'
implementation 'org.realityforge.org.jetbrains.annotations:org.jetbrains.annotations:1.7.0'
implementation 'com.alibaba:druid:1.2.6'
implementation 'org.apache.shardingsphere:sharding-jdbc-spring-boot-starter:4.1.1'
implementation 'com.github.yitter:yitter-idgenerator:1.0.6'

5 配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
yml复制代码spring:
shardingsphere:
datasource:
names: master-test0,master-test1,slave-test0,slave-test1 # 数据源节点名字
# master-test0表示主节点的test0库,master-test1表示主节点的test1库
# slave-test0表示从节点的test0库,slave-test1表示从节点的test1库
master-test0:
type: com.alibaba.druid.pool.DruidDataSource # 连接池
url: jdbc:mysql://127.0.0.1:3306/test0 # 主节点的test0库
username: root
password: 123456
master-test1:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3306/test1 # 主节点的test1库
username: root
password: 123456
slave-test0:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3307/test0 # 从节点的test0库,端口3307
username: root
password: 123456
slave-test1:
type: com.alibaba.druid.pool.DruidDataSource
url: jdbc:mysql://127.0.0.1:3307/test1 # 从节点的test1库,端口3307
username: root
password: 123456
sharding:
default-database-strategy:
inline:
sharding-column: age # 按照哪一列分库
algorithm-expression: master-test$->{age % 2} # 分库规则为对年龄取模
tables:
user:
actual-data-nodes: master-test$->{0..1}.user$->{0..2} # 分表的节点,格式为 [数据源.表名]
table-strategy:
inline:
sharding-column: id # 按照哪一列分表
algorithm-expression: user$->{id%3} # 分表规则,对id取模

master-slave-rules: # 读写分离的规则
master-test0: # 哪一个主节点
master-datasource-name: master-test0 # 指定主节点名字
slave-data-source-names: slave-test0 # 指定从节点名字
master-test1:
master-datasource-name: master-test1
slave-data-source-names: slave-test1
props:
sql:
show:
true # 打印SQL

6 准备测试代码

使用MyBatis Plus Generator生成器类生成代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;

public class MyBatisPlusGenerator {
public static void main(String[] args) {
DataSourceConfig dataSourceConfig = new DataSourceConfig.Builder("jdbc:mysql://localhost:3306/test0", "root", "123456").build();
String projectPath = System.getProperty("user.dir");
StrategyConfig strategyConfig = new StrategyConfig.Builder().addInclude("user").build();
GlobalConfig globalConfig = new GlobalConfig.Builder().outputDir(projectPath + "/src/main/java").openDir(false).build();
PackageConfig packageConfig = new PackageConfig.Builder().moduleName("user").parent("com.example.demo").serviceImpl("service").build();
new AutoGenerator(dataSourceConfig).global(globalConfig).packageInfo(packageConfig).strategy(strategyConfig).execute();
}
}

实体类加上@Builder,同时设置id类型为IdType.ASSIGN_ID:

1
2
3
4
5
6
java复制代码@Builder
public class User implements Serializable {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
//...
}

修改Controller类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@RestController
@RequestMapping("/user")
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class UserController {
private final Random random = new Random();
private final UserServiceImpl service;
@GetMapping("/select")
public List<User> select(){
return service.list();
}

@GetMapping("/insert")
public boolean insert(){
return service.save(User.builder().age(random.nextInt(80)+20).name("test name").email("test@test.com").build());
}
}

同时新增一个雪花id生成器类(具体配置方法可以参考MyBatis Plus官方文档):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码import com.baomidou.mybatisplus.core.incrementer.IdentifierGenerator;
import com.github.yitter.contract.IdGeneratorOptions;
import com.github.yitter.idgen.YitIdHelper;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;

@Component
public class IdGenerator implements IdentifierGenerator {
final IdGeneratorOptions options = new IdGeneratorOptions((short) 1);

@PostConstruct
public void init() {
YitIdHelper.setIdGenerator(options);
}

@Override
public Long nextId(Object entity) {
return YitIdHelper.nextId();
}
}

7 测试

刷新几次插入页面:

1
bash复制代码http://localhost:8080/user/insert

从输出可以看到插入都是在主节点中进行的:

在这里插入图片描述

而查询的时候:

1
bash复制代码http://localhost:8080/user/select

输出如下:

在这里插入图片描述

是在从节点查询的。

8 参考代码

Java版:

  • Github
  • 码云
  • GitCode

Kotlin版:

  • Github
  • 码云
  • GitCode

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

带你见识一下,JAVA中的方法爆炸!

发表于 2021-07-07

原创:小姐姐味道(微信公众号ID:xjjdog),欢迎分享,转载请保留出处。

要想了解Java的API有多变态,就不得不提一下队列这个接口,许多工作多年的人,依然是对此非常迷惑。虽然队列是计算机算法中的一个基本结构,但它并不仅仅只有add这个方法。

读完本文,再看到add、offer、put,不要犯晕了!

  1. 一段小代码

猜猜下面的代码会输出啥?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码void run(Callable<Object> c){
try{
System.out.println(c.call());
}catch (Exception ex){
System.out.println(ex);
}
}
void testSynchronousQueue(){
Queue<Integer> q1 = new SynchronousQueue();
run(()-> q1.add(1));

Queue<Integer> q2 = new SynchronousQueue();
run(()-> q1.offer(1));
}

实在是让人非常失望,两次执行都失败了。

1
2
bash复制代码java.lang.IllegalStateException: Queue full
false

第一次,使用add方法,程序抛出了异常,表示队列满了;第二次,程序返回了false,证明添加失败。既然无法向队列中添加元素,又没有指定队列大小的地方。那这个队列,有什么鸟用!

  1. Queue的方法

在了解这个队列的使用之前,我们来看一下Queue接口所定义的方法。

  • add(E e) 插入一个元素到队列的尾部。如果无法插入,则抛出异常
  • offer(E e) 插入一个元素到队列的为
  • E remove() 从队列头移除一个元素,如果队列为空,则抛出异常
  • E poll() 从队列头移除一个元素,如果队列为空,则返回null
  • E element() 查看对头元素,如果队列为空,则抛出异常
  • E peek() 查看对头元素,如果队列为空,则返回null

可以看到,对队列的基本操作,只有三个:插入新元素、查看队头、队头出对。根据是否抛出异常,又分为了两类。3x2=6,共6个方法。

喜欢刷题的同学,常用的肯定是offer、poll、peek,这样可以免去恼人的异常处理。平常的编码,也推荐使用非异常的api,但Java为什么提供了两套方法,来供我们使用呢?

原因就是,Queue接口继承了Collection接口,而add和remove等方法,是属于Collection接口的,Queue不得不实现一套。事实上,add方法直接调用了offer方法,为什么多出这么一套api来,真的是个谜。

1
2
3
4
5
6
java复制代码public boolean add(E e) {
if (offer(e))
return true;
else
throw new IllegalStateException("Queue full");
}

不抛异常,就容易被遗忘处理,确实是个比较牵强的原因。就凭这,能让人在这么重要的基础类库里面,创造出这么多不同名称的方法么?

  1. Put和Take

相比较上面让人纠结的add和offer,put和take方法就确实有用了。但put和take是不属于Queue接口的,它的归属是BlockingQueue。不好意思,一不小心就跳到concurrent包了。

put和take,意味着阻塞。如果操作不成功,它就一直在那里阻塞。想要它们能够正常运行下去,就需要有多个线程的配合。下面的代码会往队列里发送一个1,然后take方法拿出它,进行打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码void testBlockingSynchronousQueue() throws InterruptedException {
BlockingQueue<Integer> q1 = new SynchronousQueue();
new Thread(()-> {
try {
q1.put(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
new Thread(()-> {
try {
System.out.println(q1.take());
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}

所以,我们来看一下这些这对方法。

  • put(E e) 插入元素,如果队列满了,它会一直阻塞等待
  • E take() 获取队头元素,如果队列为空则一直等待

可以看到put和take配合起来,很容易实现一个线程安全的生产者消费者模型。相比较使用Queue的接口方法,我们只能通过死循环去检测,相比较阻塞的方式就特别节省资源。

但是还没完。阻塞的take和put方法,只能被interrupt,如何让程序阻塞等待一段时间,然后恢复运行呢?那就只有加入一个带时间戳的阻塞方法。

BlockingQueue选择了offer和poll方法,而不是take和put,暂也搞不懂到底是为什么。

  • E poll(long timeout, TimeUnit unit)
  • boolean offer(E e, long timeout, TimeUnit unit) 依然是有返回值的
  1. 你以为这样就完了?

你以为这样就完了?并没有。我们需要把目光投向LinkedList,传说中几行代码实现LRU缓存的类。

ArrayList是一个比较纯净的List,仅仅实现了List接口,但LinkedList就胃口大了一些。由于API设计者,尽最大可能想让这个链表功能更强大一些,它继承了Deque接口。由于Deque继承了Queue,所以这个链表不仅仅是个队列,还是个双向队列。

所以,它们又多了一堆API,分别来描述到底是在队头还是队尾进行操作。

  • addFirst 操作队头,加入元素
  • addLast 操作队尾,加入元素
  • offerFirst 操作队头,加入元素
  • offerLast 操作队尾,加入元素
  • removeFirst 操作队头,删除元素
  • removeLast 操作队尾,删除元素
  • pollFirst 操作队头,删除元素
  • pollLast 操作队尾,删除元素
  • getFirst 获取队头元素,类似element。TMD,这里为什么不用element?
  • getLast 获取队尾元素
  • peekFirst 获取队头元素
  • peekLast 获取队尾元素

当然,这里还有pop和push,pop=removeFirst,push=addFirst。//建议不要用,太难记了。

很好很好,由于有了头和尾的概念,api的大小变成了3x2x2=12个!加上原来的那6个,共18个(直接把pop和push忽略)。

你要说,怎么没有take和put这种阻塞的方法啊。原因就是LinkedList并不是并发的集合,你要找的功能,在LinkedBlockingDeque中,肯定会有takeFirst、takeLast、putLast、putFirst等。

  1. 队列大小

反过头来再看我们刚开始的SynchronousQueue,为什么无论向里面添加元素,还是提取元素,都会返回失败?它的容量到底是多少?

这是一个非常奇葩的类,它的内部容量是0!已经被硬编码进代码里了。

1
2
3
java复制代码public int size() {
return 0;
}

它仅仅建立了一个通道,一旦有生产,消费者就能立马拿到它,它本身是不不存任何数据的。Executors.newCachedThreadPool()就使用了SynchronousQueue。

常用的LinkedBlockingQueue、ArrayBlockingQueue,都是有界的。

但这里有一个比较奇葩的是类,加偶走ConcurrentLinkedQueue,从名字可以看出来,它并不是一个阻塞的并发类,所以并没有take和put等方法。另外,它是无界的,使用时要特别小心。你或许说,我每次判断它的size()方法来看一下是否越界不就行了。

1
2
3
4
5
6
7
8
9
java复制代码public int size() {
int count = 0;
for (Node<E> p = first(); p != null; p = succ(p))
if (p.item != null)
// Collection.size() spec says to max out
if (++count == Integer.MAX_VALUE)
break;
return count;
}

如上代码,这就是比较坑的地方,size方法,并不是O(1)时间级别的。xjjdog就曾在上面吃过大亏,最后还是不敢乱用了。

End

从上面的描述可以看出来。对于一个队列,有三套接口:插入、弹出、检测;根据是否抛异常,又分为两套,一套会抛出异常,另外一套直接返回值,刷题党自然喜欢后者了;如果再加上双向的队列,就需要再区分对头队尾;如果是阻塞队列,还要再加上一个维度。

所以,对于一个阻塞的双向队列,它的基本操作方法有:(3[基本]x2[异常与返回值]+4[阻塞加超时])x3[队头队尾]=5x2x3=30个方法,这就是王者LinkedBlockingDeque。

作者简介:小姐姐味道 (xjjdog),一个不允许程序员走弯路的公众号。聚焦基础架构和Linux。十年架构,日百亿流量,与你探讨高并发世界,给你不一样的味道。我的个人微信xjjdog0,欢迎添加好友,进一步交流。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring GraphQL成为Spring顶级项目,将发布

发表于 2021-07-07

七月五号,Spring GraphQL项目正式从experimental(实验项目)移除,现在它是一个Spring顶级项目了。并且我从消息人士得知即将发布第一个里程碑版本。

Spring GraphQ 里程碑版本规划

该项目由GraphQL Java团队和Spring团队合作开发。

GraphQL Java到现在已经6年了,日臻成熟。一直以来GraphQL Java 只是一个执行 GraphQL 请求的引擎,只关注HTTP和IO切面。现在人们需要一个真正的HTTP GraphQL 适配器。过去的12个月里GraphQL Java 和 Spring 团队之间进行了广泛的合作和讨论以实现这一目标。

这个项目对于 GraphQL Java 和更广泛的 GraphQL 生态系统来说是一个巨大的进步:由 Spring 工程师维护和发展的 Spring 集成是 GraphQL 成功的关键因素。

Spring GraphQL是GraphQL Java Spring的继承者。目的是让 Spring GraphQL 成为所有 GraphQL 应用程序的基础,进而构建在 GraphQL Java 上。

我们对 GraphQL Java 和 Spring GraphQL 的总体理念是不偏不倚,专注于全面和广泛的支持。我们希望 Spring 和 GraphQL Java 的结合构建在 Spring GraphQL 上,而不是搞花活和开发一些自以为是的功能。

GraphQL Java 团队和 Spring 团队将会在9月的Spring One大会上对Spring GraphQL进行主题演讲。

关于GraphQL

GraphQL 是一种针对 Graph(图状数据)进行查询特别有优势的 Query Language(查询语言),换个方式说它就是一种描述客户端如何向服务端请求数据的API语法,和 RESTful 规范类似。

REST和GraphQL的区别

它是由Facebook 2015年开源的规范。它的设计初衷是想要用类似图的方式表示数据,即不像在RESTful中,数据被各个API endpoint所分割,而是有关联和层次结构的被组织在一起。更多相关知识可以去GraphQL 官网了解。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

生产环境遇到一个 Go 问题,整组人都懵逼了

发表于 2021-07-07

微信搜索【脑子进煎鱼了】关注这一只爆肝煎鱼。本文 GitHub github.com/eddycjy/blo… 已收录,有我的系列文章、资料和开源 Go 图书。

大家好,我是煎鱼。

前段时间正在疯狂写代码的时候,突然有一个读者给我提了一个问题,让我有了一定的兴趣:

image.png

我还是比较感兴趣的,因为是生产环境、有代码,且整组人都懵逼的问题。

在征求了小伙伴的意见后,今天分享出来,大家也思考一下原因,一起规避这个 “坑”。

案例一

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
golang复制代码type MyErr struct {
Msg string
}

func main() {
var e error
e = GetErr()
log.Println(e == nil)
}

func GetErr() *MyErr {
return nil
}

func (m *MyErr) Error() string {
return "脑子进煎鱼了"
}

请思考一下,这段程序的输出结果是什么?

该程序所调用的 GetErr 方法所返回的是 nil,而外部判断是 e == nil,因此最终的输出结果是 true,对吗?

输出结果如下:

1
yaml复制代码2021/04/04 08:39:04 false

答案是:false。

案例二

代码示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
golang复制代码type Base interface {
do()
}

type App struct {
}

func main() {
var base Base
base = GetApp()

log.Println(base)
log.Println(base == nil)
}

func GetApp() *App {
return nil
}
func (a *App) do() {}

请思考一下,这段程序的输出结果是什么?

该程序调用了 GetApp 方法,该方法返回的是 nil,因此其赋值的 base 也是 nil。因此判断 base == nil 的最终输出结果是 <nil> 和 true,对吗?

输出结果如下:

1
2
yaml复制代码2021/04/04 08:59:00 <nil>
2021/04/04 08:59:00 false

答案是:<nil> 和 false。

为什么

为什么,这两段 Go 程序是怎么回事…也太反直觉了?其背后的原因本质上还是对 Go 语言中 interface 的基本原理的理解。

在案例一中,虽然 GetErr 方法确实是返回了 nil,返回的类型也是具体的 *MyErr 类型。但是其接收的变量却不是具体的结构类型,而是 error 类型:

1
2
golang复制代码var e error
e = GetErr()

在 Go 语言中, error 类型本质上是 interface:

1
2
3
golang复制代码type error interface {
Error() string
}

因此兜兜转转又回到了 interface 类型的问题,interface 不是单纯的值,而是分为类型和值。

所以传统认知的此 nil 并非彼 nil,必须得类型和值同时都为 nil 的情况下,interface 的 nil 判断才会为 true。

在案例一中,结合代码逻辑,更符合场景的是:

1
2
3
golang复制代码var e *MyErr
e = GetErr()
log.Println(e == nil)

输出结果就会是 true。

在案例二中,也是一样的结果,原因也是 interface。不管是 error 接口(interface),还是自定义的接口,背后原理一致,自然也就结果一致了。

总结

今天这篇文章,相当于是《Go 面试题:Go interface 的一个 “坑” 及原理分析》的变形了,毕竟是生产环境的代码改造而来,更贴合真实的实际场景。

下意识的直觉有时候不是绝对正确的,我们要正确的理解 Go 语言中的那些知识点,才能更好地实现早下班的理想和愿景。

若有任何疑问欢迎评论区反馈和交流,最好的关系是互相成就,各位的点赞就是煎鱼创作的最大动力,感谢支持。

文章持续更新,可以微信搜【脑子进煎鱼了】阅读,回复【000】有我准备的一线大厂面试算法题解和资料;本文 GitHub github.com/eddycjy/blo… 已收录,欢迎 Star 催更。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Mac 环境安装并配置终端神器 oh-my-zsh 打开 z

发表于 2021-07-07

工欲善其事,必先利其器。玩转 macOS 的终端.

第一步,安装 HomeBrew
作为 macOS 必备的包管理工具,相信大家肯定已经很熟悉了,没安装的朋友可以执行下面命令装一下,安装过的可以执行下面命令可以进行更新。

1
js复制代码/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

第二步,更新 zsh、git
macOS 一般会自带 zsh,不过版本会比较早,我们先更新一下,以便使用最新特性。

1
js复制代码brew install zsh
1
2
3
4
js复制代码==> Downloading https://homebrew.bintray.com/bottles/zsh-5.7.1.high_sierra.bottle.tar.gz
######################################################################## 100.0%
==> Pouring zsh-5.7.1.high_sierra.bottle.tar.gz
/usr/local/Cellar/zsh/5.7.1: 1,515 files, 13.3MB

第三步,切换至 zsh 并安装 oh-my-zsh
查看当前使用的 shell

1
2
3
js复制代码$ echo $SHELL

/bin/bash

查看安装的 shell

1
2
3
4
5
6
7
8
js复制代码$ cat /etc/shells

/bin/bash
/bin/csh
/bin/ksh
/bin/sh
/bin/tcsh
/bin/zsh

切换为 zsh

1
js复制代码chsh -s /bin/zsh

重启终端即可使用 zsh。

接下来安装 oh-my-zsh

1
js复制代码sh -c "$(curl -fsSL https://raw.github.com/robbyrussell/oh-my-zsh/master/tools/install.sh)"

安装完成后,终端展示如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
js复制代码____  / /_     ____ ___  __  __   ____  _____/ /_  
/ __ \/ __ \ / __ `__ \/ / / / /_ / / ___/ __ \
/ /_/ / / / / / / / / / / /_/ / / /_(__ ) / / /
\____/_/ /_/ /_/ /_/ /_/\__, / /___/____/_/ /_/
/____/ ....is now installed!


Please look over the ~/.zshrc file to select plugins, themes, and options.

p.s. Follow us at https://twitter.com/ohmyzsh.

p.p.s. Get stickers and t-shirts at http://shop.planetargon.com.

第四步,配置 oh-my-zsh
看到这里,安装流程已经完毕啦,执行最后的配置,就可以进行体验了。

打开 oh-my-zsh 配置文件

打开 zshrc 文件进行编辑,也可以使用 vim 编辑器

1
js复制代码open ~/.zshrc

本人使用的是 vs code

1
js复制代码open ~/.zshrc -a Visual\ Studio\ Code

主题

配置项 ZSH_THEME 即为 oh-my-zsh 的主题配置,oh-my-zsh 的 GitHub Wiki 页面提供了 主题列表
当设置为 ZSH_THEME=random 时,每次打开终端都会使用一种随机的主题。

插件

plugins=(git osx autojump zsh-autosuggestions zsh-syntax-highlighting)
注意:其中 zsh-autosuggestions 和 zsh-syntax-highlighting 是自定义安装的插件,需要用 git 将插件 clone 到指定插件目录下:

自动提示插件

1
js复制代码git clone git://github.com/zsh-users/zsh-autosuggestions $ZSH_CUSTOM/plugins/zsh-autosuggestions

语法高亮插件

1
js复制代码git clone git://github.com/zsh-users/zsh-syntax-highlighting $ZSH_CUSTOM/plugins/zsh-syntax-highlighting

需要其他插件的可以自行安装,如果插件未安装,开启终端的时候会报错,按照错误提示,安装对应的插件即可。

更新配置

1
js复制代码source ~/.zshrc

更新完配置即可生效.

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

我的docker随笔:开篇

发表于 2021-07-07

李迟按:

自换新工作来,几乎没有再更新博客了。在新单位中,陆续接触、学习、使用docker,再推行容器化开发,基于docker的CICD,了解kubernetes,并开始研究docker源码,时光荏苒,一下子已经过去大半年了,对docker也算有点使用体会。这个系列文章是围绕docker展开的,记录与之相关的技术点滴。由于是随笔而不是教程,写起来可能有些简陋,也不会过多涉及技术原理方面,毕竟我也不年轻了,学究习惯不多,一切以解决实际问题为主。写出来,一方面是提供日后查阅,工作多年,对于某些知识点,我还是要翻看旧时文章才能找到方法。另一方面,网络上关于docker的资料,五花八门,参差不齐,错误的有之,不实用的有之,所以想以自身实践的经验写一写。再者,如能帮助一二网友,那是庆幸的事,但万一误人子弟,却非本意。
诚惶诚恐,如履薄冰,不当之处,万望指正批评,笔者根据实际情况修正。

心得体会

  1. 对于docker,每个人看法不同。但是,有的资料、书籍似乎有夸大其作用之嫌。对我来说,docker在一些开发环境、程序运行应用场合中,表现出来的作用非常大。
  2. docker发展非常快,有些市面上的书籍比较使用的版本比较旧,有的网络资料也过时,建议阅读时一定要看清docker版本号以及写作时间(当然,这点也包括本系列文章),文章只能作参考,还是要动手摸索。另外也建议看看官方文档。
  3. docker国内下载比较慢,但是可以使用国内镜像市场的加速器(比如阿里云加速器)以提高速度。
  4. docker官方的镜像市场,可以和gitlab或github配合进行自动化构建,并且将生成的docker镜像存储在dockerhub上。
  5. 对于代码或脚本不方便公开又不想花费金钱托管镜像的,可以使用gitlab托管代码,并使用阿里云镜像服务(此服务免费提供)。
  6. google有自己的docker镜像市场(在kubernetes应用中大量使用gcr镜像),但由于众所周知的原因,国内几乎无法访问。但是,可以利用dockerhub做中转。

使用经验

2021年3月:

docker 与 docker-compose:

如果是简单的验证,可用 docker 直接运行,如果在实践中使用,建议使用 docker-compose,方便快捷。

时区:

一般镜像时区为 UTC,如果在运行容器时传递参数设置时区。如东八区:

1
2
ini复制代码environment:
- TZ=Asia/Shanghai

(个人)资源

docker仓库大本营:

hub.docker.com/

或

hub.docker.com/

个人积累的资源如下(会持续更新):

docker-compose文件:

github.com/latelee/doc…

dockerfile文件:

github.com/latelee/doc…

个人docker镜像:

hub.docker.com/u/latelee/

个人阿里云镜像仓库地址:

1
bash复制代码registry.cn-hangzhou.aliyuncs.com/latelee

登陆阿里云镜像仓库命令:

1
css复制代码docker login --username=latelee@163.com registry.cn-hangzhou.aliyuncs.com

个人docker加速地址:

1
arduino复制代码https://a8qh6yqv.mirror.aliyuncs.com

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…617618619…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%