支付功能实战(2):分布式事务介绍/单例模式介绍

思考:

  • 在项目中哪里用到单例模式?

一、解决分布式事务问题

谈分布式事务,要区分内部的和外部的,相同点是什么

1.1、产生分布式事务原因

产生原因:
用户支付成功,在第三方比如银联系统的数据库修改成功,但是可能商户端的支付数据库未修改成功

同步回调(前台通知):第三方支付系统以浏览器重定向形式将支付结果给商户端

异步回调(后台通知,用户感觉不到):第三方支付系统使用类似于HttpClient技术调用商户接口进行通知

为什么调用银联系统要用RPC调用而不同MQ?
因为HTTP技术可以跨语言跨平台,银联系统全国很多系统都有调用,不可能给每个系统创建队列 

 

1.2、跨系统中如何解决分布式事务

采用最终一致性问题 双方可以短暂一致性(遵守CAP、BASE理论),但是最终一定实现一致性问题,柔性事务

采用通知补偿性

1.3、支付回调中需要考虑的问题

1、LCN、MQ 适合于内部系统实现服务指间分布式事务解决方案,如果产生跨平台之间分布式事务如何解决呢?

    答:终一致性概念。

2、在网络延迟情况下,回调接口出现重试时,如何保证接口幂等性问题?

    答:(全局id) 重试机制是间隔性

3、在网络延迟情况下,第三方系统没有及时的将支付结果通知给商户端,存在支付状态不一致时,如何解决?

      答:调用第三方系统主动查询支付状态

4、商户系统回调接口中,如果存在代码问题,或者是商户系统宕机,导致第三方支付系统一直在重试? 最后放弃重试 如何解决呢?

      答:

    (1)手动补偿机制+日志记录 主动调用第三方接口查询

    (2)写一个定时Job,每天定时检查数据

      答:在异步回调中,使用预支付金额与回调真实支付金额进行比对,如果不一致的话,说明该交易信息存在异常。使用日志+对账核查

5、支付金额与商品金额如果不一致时,如何处理?

在异步回调中,使用预支付金额与回调真实支付金额进行比对,如果不一致的话,说明该交易信息存在异常。

使用日志+对账核查

 

6、支付服务如何与其他系统保证分布式事务问题?——外部系统

采用第三方支付流程 。实现补偿和重试机制、遵循最终一致性 、Base理论和CAP理论

7、支付系统与内部系统中出现分布式事务怎么解决?——内部系统

采用LCN、 MQ 、TCCC等技术

8、外部系统与内部系统中出现分布式事务问题有什么相同点?()

都是最终一致性问题

 

二、单例模式的使用

静态代码块方式(饿汉式)——线程安全:

静态代码块中的代码在使用类的时候就已经执行了,所以可以应用静态代码块的这个特性来实现单例设计模式

封装雪花算法(twitter的),使其变成单例的,使用雪花算法时先调用这个代码:

public class SnowflakeIdUtils {
	private static SnowflakeIdWorker idWorker;
	static {
		// 使用静态代码块初始化 SnowflakeIdWorker
		idWorker = new SnowflakeIdWorker(1, 1);
	}
	public static String nextId() {
		return idWorker.nextId() + "";
	}

}

每次使用时不可能把 工具类这样的代码初始化一次(有些类不可能让他每次都初始化,尤其是工具类):

SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1); (原雪花算法里边的)

所以可以把它做成单例的,保证一个接口里面只有一个实例。把它放在静态代码块里面让他初始化,初始化完了之后,再写一个方法进行实例对象的的调用

比如在这里后期别人只需要调用nextId()这个方法就可以用整个雪花算法去生成ID了。

静态初始化器中创建(饿汉式)——线程安全:

 public class Singleton {
       //在静态初始化器中创建单例实例,这段代码保证了线程安全
        private static Singleton uniqueInstance = new Singleton();
        //Singleton类只有一个构造方法并且是被private修饰的,所以用户无法通过new方法创建该对象实例
        private Singleton(){}
        public static Singleton getInstance(){
            return uniqueInstance;
        }

静态初始化器中创建(懒汉式)——非线程安全(默认)

(单例实例在第一次被使用时构建,而不是JVM在加载这个类时就创建)


public class Singleton {  
      private static Singleton uniqueInstance;  
      private Singleton (){
      }   
      //没有加入synchronized关键字的版本是线程不安全的
      public static Singleton getInstance() {
          //判断当前单例是否已经存在,若存在则返回,不存在则再建立单例
	      if (uniqueInstance == null) {  
	          uniqueInstance = new Singleton();  
	      }  
	      return uniqueInstance;  
      }  

如果需要线程安全,在getInstance方法加上synchronized关键字即可

参考:什么是单例模式

 

 

 

三、Token和雪花算法的工具类

3.1、Token的工具类

@Component
public class GenerateToken {
	@Autowired
	private RedisUtil redisUtil;

	/**
	 * 生成令牌
	 * 
	 * @param prefix
	 *            令牌key前缀
	 * @param redisValue
	 *            redis存放的值
	 * @return 返回token
	 */
	public String createToken(String keyPrefix, String redisValue) {
		return createToken(keyPrefix, redisValue, null);
	}

	/**
	 * 生成令牌
	 * 
	 * @param prefix
	 *            令牌key前缀
	 * @param redisValue
	 *            redis存放的值
	 * @param time
	 *            有效期
	 * @return 返回token
	 */
	public String createToken(String keyPrefix, String redisValue, Long time) {
		if (StringUtils.isEmpty(redisValue)) {
			new Exception("redisValue Not nul");
		}
		String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
		redisUtil.setString(token, redisValue, time);
		return token;
	}

	public void createListToken(String keyPrefix, String redisKey, Long tokenQuantity) {
		List<String> listToken = getListToken(keyPrefix, tokenQuantity);
		redisUtil.setList(redisKey, listToken);
	}

	public List<String> getListToken(String keyPrefix, Long tokenQuantity) {
		List<String> listToken = new ArrayList<>();
		for (int i = 0; i < tokenQuantity; i++) {
			String token = keyPrefix + UUID.randomUUID().toString().replace("-", "");
			listToken.add(token);
		}
		return listToken;

	}

	public String getListKeyToken(String key) {
		String value = redisUtil.getStringRedisTemplate().opsForList().leftPop(key);
		return value;
	}

	/**
	 * 根据token获取redis中的value值
	 * 
	 * @param token
	 * @return
	 */
	public String getToken(String token) {
		if (StringUtils.isEmpty(token)) {
			return null;
		}
		String value = redisUtil.getString(token);
		return value;
	}

	/**
	 * 移除token
	 * 
	 * @param token
	 * @return
	 */
	public Boolean removeToken(String token) {
		if (StringUtils.isEmpty(token)) {
			return null;
		}
		return redisUtil.delKey(token);
	}
}

3.2、具体雪花算法代码SnowflakeIdWorker

public class SnowflakeIdWorker {

	// ==============================Fields===========================================
	/** 开始时间截 (2015-01-01) */
	private final long twepoch = 1489111610226L;
	/** 机器id所占的位数 */
	private final long workerIdBits = 5L;
	/** 数据标识id所占的位数 */
	private final long dataCenterIdBits = 5L;
	/** 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) */
	private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
	/** 支持的最大数据标识id,结果是31 */
	private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);
	/** 序列在id中占的位数 */
	private final long sequenceBits = 12L;
	/** 机器ID向左移12位 */
	private final long workerIdShift = sequenceBits;
	/** 数据标识id向左移17位(12+5) */
	private final long dataCenterIdShift = sequenceBits + workerIdBits;
	/** 时间截向左移22位(5+5+12) */
	private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;
	/** 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) */
	private final long sequenceMask = -1L ^ (-1L << sequenceBits);
	/** 工作机器ID(0~31) */
	private long workerId;
	/** 数据中心ID(0~31) */
	private long dataCenterId;
	/** 毫秒内序列(0~4095) */
	private long sequence = 0L;
	/** 上次生成ID的时间截 */
	private long lastTimestamp = -1L;
	// ==============================Constructors=====================================
	/**
	 * 构造函数
	 * 
	 * @param workerId
	 *            工作ID (0~31)
	 * @param dataCenterId
	 *            数据中心ID (0~31)
	 */
	public SnowflakeIdWorker(long workerId, long dataCenterId) {
		if (workerId > maxWorkerId || workerId < 0) {
			throw new IllegalArgumentException(
					String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
		}
		if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
			throw new IllegalArgumentException(
					String.format("dataCenterId can't be greater than %d or less than 0", maxDataCenterId));
		}
		this.workerId = workerId;
		this.dataCenterId = dataCenterId;
	}

	// ==============================Methods==========================================
	/**
	 * 获得下一个ID (该方法是线程安全的)
	 * 
	 * @return SnowflakeId
	 */
	public synchronized long nextId() {
		long timestamp = timeGen();

		// 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常
		if (timestamp < lastTimestamp) {
			throw new RuntimeException(String.format(
					"Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
		}

		// 如果是同一时间生成的,则进行毫秒内序列
		if (lastTimestamp == timestamp) {
			sequence = (sequence + 1) & sequenceMask;
			// 毫秒内序列溢出
			if (sequence == 0) {
				// 阻塞到下一个毫秒,获得新的时间戳
				timestamp = tilNextMillis(lastTimestamp);
			}
		}
		// 时间戳改变,毫秒内序列重置
		else {
			sequence = 0L;
		}

		// 上次生成ID的时间截
		lastTimestamp = timestamp;

		// 移位并通过或运算拼到一起组成64位的ID
		return ((timestamp - twepoch) << timestampLeftShift) //
				| (dataCenterId << dataCenterIdShift) //
				| (workerId << workerIdShift) //
				| sequence;
	}

	/**
	 * 阻塞到下一个毫秒,直到获得新的时间戳
	 * 
	 * @param lastTimestamp
	 *            上次生成ID的时间截
	 * @return 当前时间戳
	 */
	protected long tilNextMillis(long lastTimestamp) {
		long timestamp = timeGen();
		while (timestamp <= lastTimestamp) {
			timestamp = timeGen();
		}
		return timestamp;
	}

	/**
	 * 返回以毫秒为单位的当前时间
	 * 
	 * @return 当前时间(毫秒)
	 */
	protected long timeGen() {
		return System.currentTimeMillis();
	}

	// ==============================Test=============================================
	/** 测试 */
	public static void main(String[] args) {
		System.out.println(System.currentTimeMillis());
		SnowflakeIdWorker idWorker = new SnowflakeIdWorker(1, 1);
		long startTime = System.nanoTime();
		for (int i = 0; i < 50000; i++) {
			long id = idWorker.nextId();
			System.out.println(id);
		}
		System.out.println((System.nanoTime() - startTime) / 1000000 + "ms");
	}

}

 

  上一篇: 支付功能-环境搭建

  下一篇: 支付功能-策略+工厂+反射+Token实现支付选型和表单提交

若对你有帮助,欢迎关注!!点赞!!评论!!

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值