事务隔离主要是针对ACID中的 I 隔离性设置的,在了解这几种数据库事务之前需要先了解几个重要概念:

  • 脏读 - 事务1(READ)访问了事务2(WRITE)未提交的事务

    2154loev8R53

  • 幻读 - 一个事务读取了2次,得到的结果不一样

    2155aZxX8o26

  • 不可重复读 - 一个事务读取同一条记录2次,得到的结果不一致

    2156wuH3uE40

为了解决上面的三个问题,有四种数据库隔离级别

  1. READ UNCOMMITTED

    故名思义,读未提交,允许脏读,当然也会出现不可重复读和幻读,可以通过“排他写锁”实现,在表对某行进行修改时,会对该行加上行共享锁

  2. READ COMMITTED

    读提交,允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。在本事务未提交之前其他事务的增删改操作提交后会影响读的结果。 读的是最新结果 。在对表进行修改时,会对表数据行加上行共享锁

  3. Repeatable Read

    可重复读,禁止不可重复读取和脏读取,但是有时可能出现幻读数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。读的是快照结果。MysQL 默认级别,通过MVCC解决了幻读问题。在事务中对某条记录修改,会对记录加上行共享锁,直到事务结束才会释放。

  4. Serializable

    序列化,提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,不能并发执行。仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

    进行查询时就会对表或行加上共享锁,其他事务对该表将只能进行读操作,而不能进行写操作。

    2203eTpuhO46

事务隔离级别越高,数据的完整性和一致性越好,但会影响并发性能。mysql默认使用 Repeatable Read 级别,而aliyun rds 默认使用 READ COMMITTED。

查看查询数据库隔离级别

1
select @@global.tx_isolation;

Mysql 8.0 以上需要使用

1
select @@transaction_isolation;

修改事务隔离级别:mysql.ini

1
2
3
#可选参数有:READ-UNCOMMITTED, READ-COMMITTED, REPEATABLE-READ, SERIALIZABLE.
[mysqld]
transaction-isolation = REPEATABLE-READ

当然,也可以只对当前session修改

1
mysql> set session transaction isolation level read uncommited;

锁机制

共享锁:由读表操作加上的锁,加锁后其他用户只能获取该表或行的共享锁,不能获取排它锁,也就是说只能读不能写

排它锁:由写表操作加上的锁,加锁后其他用户不能获取该表或行的任何锁,典型是mysql事务中的

锁的范围:

行锁: 对某行记录加上锁

表锁: 对整个表加上锁

References

《高性能mysql》

https://developer.aliyun.com/article/4281

[阿里云上的rds 的隔离级别read committed 而不是repeatable-read设置原因](

简单介绍一下redis的几种数据类型: StringsListsSetsHasheSorted setsBitmaps and HyperLogLogs

阅读全文 »

base64是一种基本的加密算法,在Java中可以使用java自带的base64编码,也可以用apache 的commons-codec包。最近在使用commons-codec 1.10 版本能正常解密微信的消息,升级为1.13后出现了不能正常decode,出现异常

1
2
3
4
5
6
java.lang.IllegalArgumentException: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible value

at org.apache.commons.codec.binary.Base64.validateCharacter(Base64.java:798)
at org.apache.commons.codec.binary.Base64.decode(Base64.java:477)
at org.apache.commons.codec.binary.BaseNCodec.decode(BaseNCodec.java:411)
at org.apache.commons.codec.binary.BaseNCodec.decode(BaseNCodec.java:395)
  1. 具体场景

    处理微信公众号消息时,对消息内容进行必须的加解密,出现的问题是处理aesKey时出现的,具体demo如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    public WxOpenCryptUtil(WxOpenConfigStorage wxOpenConfigStorage) {
    /*
    * @param token 公众平台上,开发者设置的token
    * @param encodingAesKey 公众平台上,开发者设置的EncodingAESKey
    * @param appId 公众平台appid
    */
    String encodingAesKey = wxOpenConfigStorage.getComponentAesKey();
    String token = wxOpenConfigStorage.getComponentToken();
    String appId = wxOpenConfigStorage.getComponentAppId();

    this.token = token;
    this.appidOrCorpid = appId;
    this.aesKey = Base64.decodeBase64(encodingAesKey + "=");
    }

    当升级commons-codec版本为1.13及以上时,会出现上述异常

  2. 出现的原因

    1.13出现异常的方法

    1
    2
    3
    4
    5
    6
    7
    private long validateCharacter(final int numBitsToDrop, final Context context) {
    if ((context.ibitWorkArea & numBitsToDrop) != 0) {
    throw new IllegalArgumentException(
    "Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible value");
    }
    return context.ibitWorkArea >> numBitsToDrop;
    }

    仔细分析可以看出编解码是在BaseNCodec.java是Base64和Base32的基类

    image-20200330115937553

    可以看出唯一的差别就是在解码时对参数做了校验。有必要了解下这个参数校验做了些什么?

    1
    2
    > context.ibitWorkArea & numBitsToDrop
    >

    ibitWorkArea: 位处理的基本位数

    numBitsToDrop: 应该为空的低位数目

    可以看出当 context.ibitWorkArea & numBitsToDrop不为0时就会抛出异常,实际上只有base64严格模式编码下,才可能会为0,松散模式不会为0

  3. 解决办法

    降低版本到1.12以下可以解决该问题,或者等commons-codec版本更新到1.15,最新的源码已经处理了该问题

    1
    2
    3
    4
    5
    6
    7
    private void validateCharacter(final int emptyBitsMask, final Context context) {
    if (isStrictDecoding() && (context.ibitWorkArea & emptyBitsMask) != 0) {
    throw new IllegalArgumentException(
    "Strict decoding: Last encoded character (before the paddings if any) is a valid base 64 alphabet but not a possible encoding. " +
    "Expected the discarded bits from the character to be zero.");
    }
    }
  4. 扩展

    base64的严格模式和松散模式定义,直接引用源码了

    Lenient: Any trailing bits are composed into 8-bit bytes where possible.
    The remainder are discarded.
    Strict: The decoding will raise an {@link IllegalArgumentException} if trailing bits
    are not part of a valid encoding. Any unused bits from the final character must
    be zero. Impossible counts of entire final characters are not allowed.

References

使用java8的java.util.Base64报“java.lang.IllegalArgumentException: Illegal base64 character d”的问题

Base64笔记

Base64.decode fails on Java11 for certain valid base 64 encoded String

函数式接口

先看一下官方定义

Functional interfaces provide target types for lambda expressions and method references.

可以看出函数式接口主要用于lambda表达式,这类接口只定义了唯一的抽象方法的接口(除了隐含的Object对象的公共方法),一开始也称SAM类型接口(Single Abstract Method)。

阅读全文 »

概念[来自wikipedia]

原码(True form):一个二进制数左边加上符号位后所得到的码,且当二进制数大于0时,符号位为0;二进制数小于0时,符号位为1;二进制数等于0时,符号位可以为0或1(+0/-0)

反码(One’s complement):一种在计算机中数的机器码表示。对于单个数值(二进制的0和1)而言,对其进行取反操作就是将0变为1,1变为0

补码(2’s complement):一种用二进制表示有号数的方法,也是一种将数字的正负号变号的方式,主要优点是 不需因为数字的正负而使用不同的计算方式

计算

正数:原码、反码、补码相同

负数:反码 — 符号位不变化,其余位数取反*,补码 — *符号位不变化,其余各位原码取反+1, 即 反码+1

测试

代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Test
public void byteTest() {
reverse(0);
reverse(6);
reverse(-6);
reverse(128);
reverse(-128);
reverse(1280);
reverse(-1280);
}

private Integer reverse(Integer a) {
System.out.printf("%5d\t%32s\t%5d\t%32s\t%5d\t%32s%n", a, Integer.toBinaryString(a), ~a, Integer.toBinaryString((~a)), ~a + 1, Integer.toBinaryString((~a + 1)));
return ~a + 1;
}

结果

1
2
3
4
5
6
7
0	                               0	   -1	11111111111111111111111111111111	    0	                               0
6 110 -7 11111111111111111111111111111001 -6 11111111111111111111111111111010
-6 11111111111111111111111111111010 5 101 6 110
128 10000000 -129 11111111111111111111111101111111 -128 11111111111111111111111110000000
-128 11111111111111111111111110000000 127 1111111 128 10000000
1280 10100000000 -1281 11111111111111111111101011111111 -1280 11111111111111111111101100000000
-1280 11111111111111111111101100000000 1279 10011111111 1280 10100000000

可以看出:

  • 对一个数字直接取反,并不是其相反数,需要+1

  • [+0]原码=0000 0000, [-0]原码=1000 0000

    [+0]反码=0000 0000, [-0]反码=1111 1111

    [+0]补码=0000 0000, [-0]补码=0000 0000

众所周知,开源代码都有其开源许可证,详细选择参考如何选择开源许可证?。这里不是要讨论开源许可证的问题,而是开源贡献协议。最近在参加一个开源项目时,提到需要使用DCO贡献协议,所以就去了解当下主流的开源贡献协议。

CLA(Contributor License Agreement)协议

CLA时对开源License的法律性质补充,多由企业或者组织自行定义,作为开源协议的补充,一次性签署,比如alibaba CLA协议: Alibaba Open Source Individual CLA

使用上来说,CLA只需要签署一次,如阿里巴巴个人CLAGoogle CLAPivotal CLACNCF CLA

使用CLA的用户或者组织

  • Facebook
  • Eclipse
  • Go
  • Google
  • InfluxDB
  • Python
  • Elastic
  • CNCF
  • …..

DCO(Developer Certificate of Origin)协议

DCO时Linux Foundation提出的,只有四条简短条文, 具体内容如下: DCO,使用上只需要开发者提交信息时追加 Signed-off-by 即可。具体内容(version1.1)如下:

  1. 该贡献全部或部分由我创建,我有权根据文件中指明的开源许可提交;要么
  2. 该贡献是基于以前的工作,这些工作属于适当的开源许可,无论这些工作全部还是部分由我完成,我有权根据相同的开源许可证(除非我被允许根据不同的许可证提交)提交修改后的工作;要么
  3. 该贡献由1、2、或 3 证明的其他人直接提供给我,而我没有对其进行修改。
  4. 我理解并同意该项目和贡献是公开的,并且该贡献的记录(包括我随之提交的所有个人信息,包括我的签字)将无限期保留,并且可以与本项目或涉及的开源许可证保持一致或者重新分配。

使用方法详见 Probot: DCO, 其实就是提交代码(commit)时增加-s参数,然后你会在提交信息里看到

1
2
3
This is my commit message

Signed-off-by: DEVELOPER <[email protected]>

使用DCO的用户或者组织

  • Gitlab
  • Chef
  • TiKv
  • Apache SkyWalking
  • ……

总结

如果你只是一个commiter,请遵从开源项目的贡献者协议,或者发起issue请求变更,如果是自己的开源项目,更看重法律风险,建议使用CLA,看重社区合作,可以使用DCO。

References

  1. CLA vs. DCO: What’s the difference?
  2. 为何《贡献者许可协议》不利于开源社区?
  3. Move from CLA to DCO #2649 - github.com
  4. probot/dco

支付宝(ISV)

参考文档:https://docs.open.alipay.com/291/106115

签名方式:RSA2

具体实现

初始化 AlipayClient

1
AlipayClient alipayClient = new DefaultAlipayClient(gateway,app_id,private_key,"json",charset,alipay_public_key,sign_type);

具体签名过程

  1. 获取encryptor,加密关键参数部分

    1
    2
    String encryptContent = getEncryptor().encrypt(
    appParams.get(AlipayConstants.BIZ_CONTENT_KEY), this.encryptType, this.charset)

    加密算法

    1
    2
    3
    4
    5
    6
    7
    8
    Cipher cipher = Cipher.getInstance(AES_CBC_PCK_ALG);

    IvParameterSpec iv = new IvParameterSpec(AES_IV);
    cipher.init(Cipher.ENCRYPT_MODE,
    new SecretKeySpec(Base64.decodeBase64(aesKey.getBytes()), AES_ALG), iv);

    byte[] encryptBytes = cipher.doFinal(content.getBytes(charset));
    return new String(Base64.encodeBase64(encryptBytes));
  1. 签名

    1
    2
    3
    String signContent = AlipaySignature.getSignatureContent(requestHolder);
    protocalMustParams.put(AlipayConstants.SIGN,
    getSigner().sign(signContent, this.signType, charset));

    签名算法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    PrivateKey priKey = getPrivateKeyFromPKCS8(AlipayConstants.SIGN_TYPE_RSA,
    new ByteArrayInputStream(privateKey.getBytes()));

    java.security.Signature signature = java.security.Signature
    .getInstance(AlipayConstants.SIGN_SHA256RSA_ALGORITHMS);

    signature.initSign(priKey);

    if (StringUtils.isEmpty(charset)) {
    signature.update(content.getBytes());
    } else {
    signature.update(content.getBytes(charset));
    }

    byte[] signed = signature.sign();

    return new String(Base64.encodeBase64(signed));
  1. 增加accessToken等参数发起访问

微信(ISV)

主要依靠token机制,支持post,get,消息体明文传输

消息内返回需要加密:

加密机制:aes

主要加密逻辑

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
public String encrypt(String plainText) {
String encryptedXml = this.encrypt(genRandomStr(), plainText);
String timeStamp = Long.toString(System.currentTimeMillis() / 1000L);
String nonce = genRandomStr();
String signature = SHA1.gen(new String[]{this.token, timeStamp, nonce, encryptedXml});
return generateXml(encryptedXml, signature, timeStamp, nonce);
}
protected String encrypt(String randomStr, String plainText) {
ByteGroup byteCollector = new ByteGroup();
byte[]a randomStringBytes = randomStr.getBytes(CHARSET);
byte[] plainTextBytes = plainText.getBytes(CHARSET);
byte[] bytesOfSizeInNetworkOrder = number2BytesInNetworkOrder(plainTextBytes.length);
byte[] appIdBytes = this.appidOrCorpid.getBytes(CHARSET);
byteCollector.addBytes(randomStringBytes);
byteCollector.addBytes(bytesOfSizeInNetworkOrder);
byteCollector.addBytes(plainTextBytes);
byteCollector.addBytes(appIdBytes);
byte[] padBytes = PKCS7Encoder.encode(byteCollector.size());
byteCollector.addBytes(padBytes);
byte[] unencrypted = byteCollector.toBytes();

try {
Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
SecretKeySpec keySpec = new SecretKeySpec(this.aesKey, "AES");
IvParameterSpec iv = new IvParameterSpec(this.aesKey, 0, 16);
cipher.init(1, keySpec, iv);
byte[] encrypted = cipher.doFinal(unencrypted);
return BASE64.encodeToString(encrypted);
} catch (Exception var14) {
throw new RuntimeException(var14);
}
}