一 课程简介
- redis缓存简单实现;(掌握)
- redis的三大问题解决方案;(掌握)
- 分布式锁-为什么;(掌握)
- 分布式锁的概述;(掌握)
- 常见的分布锁解决方案;(掌握)
- 项目实战;(掌握)
二 redis缓存的简单实现
1 哪儿用
租户类型,系统配置,数据字典(单体项目),不经常变!经常使用,命中率要高!适合放缓存
2 为什么用
- 降低数据库压力
- 增强访问速度,提高用户体验
3 怎么用
3.1集成redis
-
导入jar
<!--整合Redis , 底层可以用jedis--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> <!--排除lettuce jar包,就是不使用这个客户端,而是使用jedis--> <exclusions> <exclusion> <groupId>io.lettuce</groupId> <artifactId>lettuce-core</artifactId> </exclusion> </exclusions> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> </dependency>
-
配置
spring: application: name: gift-sysmanage #服务名 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/gift_sys?useSSL=true username: root password: 123456 redis: database: 0 host: 127.0.0.1 port: 6379 password: 123456 jedis: pool: max-wait: 2000ms min-idle: 2 max-idle: 8
package cn.ronghuanet.gift.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import javax.annotation.Resource; @Configuration public class RedisConfig { @Resource private RedisConnectionFactory factory; //使用JSON进行序列化 reids常见数据结构:String(项目用它) list set zset hash @Bean public RedisTemplate<Object, Object> redisTemplate() { RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(factory); //JSON格式序列化 GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(); //key的序列化 redisTemplate.setKeySerializer(genericJackson2JsonRedisSerializer); //value的序列化 redisTemplate.setValueSerializer(genericJackson2JsonRedisSerializer); //hash结构key的虚拟化 redisTemplate.setHashKeySerializer(new StringRedisSerializer()); //hash结构value的虚拟化 redisTemplate.setHashValueSerializer(genericJackson2JsonRedisSerializer); return redisTemplate; } }
-
测试
package cn.ronghuanet.gift; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.test.context.junit4.SpringRunner; import java.util.Arrays; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest(classes = SysmanageStart.class) public class SpringRedisTest { @Autowired private RedisTemplate redisTemplate; @Test public void test() throws Exception{ List<User> users = Arrays.asList(new User(1L, "zs"), new User(2L, "zs")); String key = "jfhhfhf"; redisTemplate.opsForValue().set(key, users); System.out.println(redisTemplate.opsForValue().get(key)); } }
3.2 实现缓存-原生
@Override
public List<TenantType> selectList(Wrapper<TenantType> wrapper) {
//从redis中查询,如果有直接返回 10000
Object o = redisTemplate.opsForValue().get(TENANT_TYPES_CACHE_IN_REDIS);
if (Objects.nonNull(o))
return (List<TenantType>) o;
synchronized (TenantTypeServiceImpl.class){
o = redisTemplate.opsForValue().get(TENANT_TYPES_CACHE_IN_REDIS);
if (Objects.nonNull(o))
return (List<TenantType>) o;
//如果没有从数据库中查询并返回
List<TenantType> tenantTypes = super.selectList(wrapper);
if (tenantTypes==null || tenantTypes.size()<1) //如果数据库没有,设置一个[],放入缓存并返回
tenantTypes = new ArrayList<>();
redisTemplate.opsForValue().set(TENANT_TYPES_CACHE_IN_REDIS, tenantTypes);
return tenantTypes;
}
}
//缓存的三大问题
//1 缓存的穿透:缓存和数据库都没有(id=-1),高并发直接打到数据库
//解决方案:1 合法性判断(不存在) 2 返回一个默认值 3 布隆过滤器
//2 缓存击穿: 某个数据库缓存没有(做增删改的删除缓存),大量的请求打到数据库库
//互斥锁
//不设置过期时间
//3 缓存的雪崩: 多个数据库缓存同时没有,少量的请求累积成大量请求
//不需要处理雪崩 双重判断,一种缓存只打一个到数据库,不存在雪崩!
//不设置过期时间
//双key
//===============做完增删改要删除缓存=================//
三 为什么需要分布式锁
1 单体项目同步实现
在单进程(启动一个jvm)的系统中,当存在多个线程可以同时改变某个变量(可变共享变量)时,就需要对变量或代码块做同步,使其在修改这种变量时能够线性执行消除并发修改变量。而同步的本质是通过锁来实现的。为了实现多个线程在一个时刻同一个代码块只能有一个线程可执行,那么需要在某个地方做个标记,这个标记必须每个线程都能看到,当标记不存在时可以设置该标记,其余后续线程发现已经有标记了则等待拥有标记的线程结束同步代码块取消标记后再去尝试设置标记。这个标记可以理解为锁。
并发问题:
解决方案:
在java中可以通过synchronize和lock等手段来实现。
2 分布式同步问题及解决方案
2.1 问题
很多时候我们需要保证一个方法在同一时间内只能被同一个线程执行。在单机环境中,通过 Java 提供的并发 API 我们可以解决,但是在分布式环境下,就没有那么简单啦。
-
分布式与单机情况下最大的不同在于其不是多线程而是多进程。
-
多线程由于可以共享堆内存,因此可以简单的采取内存作为标记存储位置。而进程之间甚至可能都不在同一台物理机上,因此需要将标记存储在一个所有进程都能看到的地方。
那么原来的方案就不行了
2.2 解决思路
分布式锁作用就是在分布式系统,保证某个方法只能在同一时间只有一个进程的一个线程执行。
在整个分布式环境下都只有一份锁。
分布式锁是多个进程共享的,多个进程保证某段代码在同一时刻只有一个线程执行使用。
多个进程能够访问的地方上把锁就ok!
2.3 分布式锁的使用场景
- 分布式环境中定时任务
-
超卖问题
-
分布式缓存-集群环境下:同步更新缓存
小结:在分布式环境下(集群下),某段代码部署多份,最终同一时刻只有一个线程能执行(多个进程的多个线程),这种场景就要用到分布式锁!
四 分布式锁概述
1 什么是分布式锁
就是在分布式环境下,保证某个公共资源只能在同一时间被多进程应用的某个进程的某一个线程访问时使用锁。
2 几个使用场景分析
一段代码同一时间只能被同一个不同进程的一个线程执行
-
库存超卖 (库存被减到 负数),上面案例就是库存超卖
-
定时任务
-
分布式缓存中缓存同步
-
转账(多个进程修改同一个账户)
3 需要什么样的分布式锁-特征
-
可以保证在分布式部署的应用集群中同一个方法在同一时间只能被一台机器上的一个线程执行。(互斥性)
-
这把锁要是一把可重入锁(避免死锁)(重入性)
-
这把锁最好是一把阻塞锁(自旋)(根据业务需求考虑要不要这条)
-
这把锁最好是一把公平锁(根据业务需求考虑要不要这条)
-
获取锁和释放锁的性能要好
-
有高可用的获取锁和释放锁功能
4 常见的分布式锁解决方案
1.4.1. 思路
-
当在分布式模型下,数据只有一份(或有限制),此时需要利用锁的技术控制某一时刻修改数据的进程数。
-
与单机模式下的锁不仅需要保证进程可见,还需要考虑进程与锁之间的网络问题。
-
分布式锁还是可以将标记存在公共内存(redis),只是该内存不是某个进程分配的内存而是公共内存如 Redis、Memcache。至于利用数据库、文件(oss),zk等做锁与单机的实现是一样的,只要保证标记能互斥就行。
在多个进程公共能够访问地方放个标识!一个进程的某个线程进去时,标识已经进去(获取到锁),其他线程就要等待!直到原来的线程释放,新的线程才能可以获取锁.
1.4.2. 分布式锁三种方式
-
基于数据库操作
-
基于redis缓存和过期时间
-
基于zookeeper 临时顺序节点+watch
从理解的难易程度角度(从低到高)数据库 > redis > Zookeeper
从实现的复杂性角度(从低到高)数据库> redis >Zookeeper
从性能角度(从高到低)redis > Zookeeper > 数据库
从可靠性角度(从高到低)Zookeeper > redis > 数据库
Zookeeper >redis>数据库(基本不用)
基于数据库基本不用,zk或redis要根据项目情况来决定,如果你项目本来就用到zk,就使用zk,否则redis,redis是项目标配
五 分布式环境互斥实现
1 数据库锁
1.1 悲观锁 innodb行锁
- 共享锁(S Lock):允许事务读一行数据,具有锁兼容性质,允许多个事务同时获得该锁。
- 排它锁(X Lock):允许事务删除或更新一行数据,具有排它性,某个事务要想获得锁,必须要等待其他事务释放该对象的锁。
X锁和其他锁都不兼容,S锁值和S锁兼容,S锁和X锁都是行级别锁,兼容是指对同一条记录(row)锁的兼容性情况。
Mysql innodb锁的默认操作:
- 我们对某一行数据进行查询是会默认使用S锁加锁,如果硬是要把查询也加X锁使用
@Select("select * from t_goods where id = #{id}")
Goods laodByIdForUpdate(Long id);
-
读的时候硬是要加x锁
@Select(“select * from t_goods where id = #{id} for update”)
Goods laodByIdForUpdate(Long id); -
当我们对某一行数据进行增删改是会加X锁
1.2 乐观锁
乐观锁是相对悲观锁而言的,乐观锁假设数据一般情况下不会造成冲突,所以在数据进行提交更新的时候,才会正式对数据的冲突与否进行检测,如果发现冲突了,则返回给用户错误的信息,让用户决定如何去做。
直接用:表中添加一个时间戳或者版本号的字段来实现,update account set version = version + 1 where id = #{id} and version = #{oldVersion} 当更新不成功,客户端重试,重新读取最新的版本号或时间戳,再次尝试更新,类似 CAS 机制,推荐使用。
2 分布式锁
疑问?既然可以使用数据库悲观锁和乐观锁保证分布式环境的互斥!那为什么还要分布式锁!
有的操作是没有数据库参与的,又想分布式环境互斥! 这是就必须使用分布式锁!
2.1 基于数据库的
-
方案1 主键
主键不能重复
package com.ronghuanet.utils; import java.sql.Connection; import java.sql.SQLException; import java.sql.Statement; //基于数据库的分布式锁实现 public class DbGoodsLock { private Long goodsId = null; public DbGoodsLock(Long goodsId) { this.goodsId = goodsId; } /** * 能插入就能获取获取锁 * @return */ public boolean trylock(){ Connection connection = null; try{ connection = JDBCUtils.getConnection(); Statement statement = connection.createStatement(); statement.execute("insert into t_goods_lock(id) values("+this.goodsId+")"); System.out.println(Thread.currentThread().getName()+"加锁,插入数据 goodsId="+goodsId); return true; }catch (Exception e) { //e.printStackTrace(); System.out.println(Thread.currentThread().getName()+"加锁异常====================:"+e.getMessage()); return false; } finally { if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } //阻塞获取锁 public void lock(){ if (trylock()) return; try { Thread.sleep(10); System.out.println("尝试获取锁..."); } catch (InterruptedException e) { e.printStackTrace(); } lock(); } //释放锁 public boolean unlock(){ Connection connection = null; try{ connection = JDBCUtils.getConnection(); Statement statement = connection.createStatement(); statement.execute("delete from t_goods_lock where id = "+goodsId); System.out.println(Thread.currentThread().getName()+"解锁,删除数据 goodsId="+goodsId); return true; }catch (Exception e) { System.out.println(Thread.currentThread().getName()+"解锁异常====================:"+e.getMessage()); //e.printStackTrace(); return false; } finally { if (connection != null) { try { connection.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }
-
方案2
唯一字段不能重复,和上面原来一样
-
l 数据库是单点?搞两个数据库,数据之键双向同步,一旦挂掉快速切换到备库上。 主备切换
-
l 没有失效时间?只要做一个定时任务,每隔一定时间把数据库中的超时数据清理一遍。
-
l 非阻塞的?搞一个 while 循环,直到 insert 成功再返回成功。
-
l 非重入的?在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。
n 获取:再次获取锁的同时更新count(+1).
n 释放:更新count-1,当count==0删除记录。
l 非公平的?-mq
数据库实现分布式锁,一般都很少用,因为对于数据库来说压力比较大,而且性能效率也不是很好,而且想实现公平锁,重入锁太麻烦了
2.2 redis
方案1:原生
1 setnx(如果不存在才设置成功)+del没有可以就添加 setnx goods_id = 1 del goods_id
2 expire+watchdog续约时间(不好做,我不做)
3 value是uuid,获取判断,删除
4 lua脚本
package com.ronghuanet.lock;
public interface IDistributedLock {
/**
* 自旋上锁
*/
void lock();
/**
* 释放锁
*/
void unlock();
/**
* 尝试获取锁
*/
boolean tryLock();
}
package com.ronghuanet.lock;
import com.ronghuanet.utils.ApplicationContextHolder;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.DefaultRedisScript;
import org.springframework.data.redis.core.script.RedisScript;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class RedisLock implements IDistributedLock {
private String resourceName;
private String lockVal; //try del都有用到uuid,所以构造的时候产生一个成员变量
private RedisTemplate redisTemplate;
//不交给spring管理就一般用不了RedisTemplate
//1 不球用了,用原生jedis-台球麻烦了
//2 想办法实现:一个不受spring管理的bean能够获取受spring管理的bean
public RedisLock(String resourceName) {
this.resourceName = resourceName;
this.lockVal = UUID.randomUUID().toString();
ApplicationContext context = ApplicationContextHolder.getApplicationContext();
redisTemplate = (RedisTemplate) context.getBean("redisTemplate");
}
@Override
public void lock() {
if (tryLock())
return;
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
lock();
}
@Override
public void unlock() {
//get check del--->lua key value
/*if redis.call('get', KEYS[1]) == KEYS[2] then
return redis.call('del', KEYS[1])
else
return 0
end*/
List<String> params = Arrays.asList(resourceName, lockVal); //goods_1 jfjjfjflfjof
redisTemplate.execute(redisScript(),params);
}
public RedisScript<Long> redisScript(){
DefaultRedisScript<Long> script = new DefaultRedisScript<>();
script.setResultType(Long.class);
//script.setScriptSource(new ResourceScriptSource(new ClassPathResource("redis.lua")));
script.setScriptText("if redis.call('get', KEYS[1]) == KEYS[2] then return redis.call('del', KEYS[1]) else return 0 end");
return script;
}
@Override
public boolean tryLock() {
//uuid setnx expire
//hash K(resourceName) k(uuid) v(count)+
// redisTemplate.opsForHash().putIfAbsent(resourceName,lockVal,1);
// redisTemplate.expire(resourceName,3,TimeUnit.SECONDS);
Boolean result = redisTemplate.opsForValue().setIfAbsent(resourceName, lockVal, 10, TimeUnit.SECONDS);
System.out.println(resourceName);
System.out.println(lockVal);
return result;
}
}
package com.ronghuanet.service.impl;
import com.ronghuanet.domain.Goods;
import com.ronghuanet.lock.IDistributedLock;
import com.ronghuanet.lock.RedisLock;
import com.ronghuanet.mapper.GoodsMapper;
import com.ronghuanet.service.IGoodsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Map;
//@Service
public class GoodsServiceImpl_redis_Lock implements IGoodsService {
// @Autowired
// private IDistributedLock lock;
@Autowired
private GoodsMapper goodsMapper;
@Override
public Goods getById(Long id) {
return goodsMapper.laodById(1L);
}
@Override
@Transactional
public void updateNum(Map<String,Object> params) {
Long goodsId = (Long) params.get("id");
Integer num = (Integer) params.get("num");
String resourceName = "goods_"+goodsId;
IDistributedLock lock = new RedisLock(resourceName);
try{
lock.lock();
System.out.println(Thread.currentThread().getName()+" get lock!");
Goods goods = goodsMapper.laodById(goodsId);
Thread.sleep(4000);
System.out.println(goods);
System.out.println(Thread.currentThread().getName()+goods.getCount()+":"+num);
if (goods.getCount()>=num){
goodsMapper.updateNum(params);
System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
}
}catch (Exception e){
e.printStackTrace();
}finally {
if (lock != null) {
lock.unlock();
}
}
}
}
方案2:框架实现
业界也提供了多个现成好用的框架予以支持分布式锁,比如Redisson、spring-integration-redis,redlock redisson,redlock 底层原理!!!1 他们的对比
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.2.3</version>
</dependency>
@Value("${spring.redis.host}")
private String redisHost;
@Value("${spring.redis.port}")
private int redisPort;
@Value("${spring.redis.database}")
private int redisdatabase;
@Value("${spring.redis.password}")
private String redisPassword;
@Bean
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer().setAddress(redisHost + ":" + redisPort);
config.useSingleServer().setDatabase(redisdatabase);
config.useSingleServer().setPassword(redisPassword);
RedissonClient redisson = Redisson.create(config);
return redisson;
}
package com.ronghuanet.service.impl;
import com.ronghuanet.domain.Goods;
import com.ronghuanet.mapper.GoodsMapper;
import com.ronghuanet.service.IGoodsService;
import com.ronghuanet.utils.RedissionUtils;
import net.bytebuddy.asm.Advice;
import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Map;
@Service
public class GoodsServiceImpl_redission implements IGoodsService {
@Autowired
private GoodsMapper goodsMapper;
@Autowired
private RedissonClient redissonClient;
@Override
public void updateNum(Map<String,Object> params) {
Long goodsId = (Long) params.get("id");
Integer num = (Integer) params.get("num");
System.out.println(Thread.currentThread().getName()+"enter!");
String resourceName = "goods" + goodsId;
RLock rLock = redissonClient.getLock(resourceName);
try{
rLock.lock();
System.out.println(Thread.currentThread().getName()+" get lock!");
Goods goods = goodsMapper.laodById(goodsId);
System.out.println(goods);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(goods.getCount()+":"+num);
if (goods.getCount()>=num){
goodsMapper.updateNum(params);
System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
}
}catch (Exception e){
e.printStackTrace();
}
finally {
if (rLock != null) {
rLock.unlock();
System.out.println(Thread.currentThread().getName()+" unlock!");
}
}
}
@Override
public Goods getById(Long id) {
return goodsMapper.laodById(1L);
}
}
2.3 zk
2.3.1 zk理论
2.3.1.1 是什么?
ZooKeeper是Apache下的一个Java开源项目(最初由Yahoo开发, 后捐献给了Apache)。
ZooKeeper的原始功能很简单,基于它的层次型的目录树的数据结构,并通过对树上的节点进行有效管理,可以设计出各种各样的分布式集群管理功能。此外, ZooKeeper本身 也是分布式的。
2.3.1.2 数据库模型
Zookeeper会维护一个具有层次关系的树状的数据结构,它非常类似于一个标准的文件系统,如下图所
示:同一个目录下不能有相同名称的节点
2.3.1.3 节点分类
ZooKeeper 节点是有生命周期的这取决于节点的类型,在 ZooKeeper 中,节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),以及时序节点(SEQUENTIAL ),具体在节点创建过程中,一般是组合使用,可以生成以下 4 种节点类型。 是否持久化,是否有序
-
持久节点(PERSISTENT)与临时节点(EPHEMERAL)
所谓持久节点,是指在节点创建后,就一直存在,直到有删除操作来主动清除这个节点——不会因为创建该节点的客户端会话失效而消失。和持久节点不同的是,临时节点的生命周期和客户端会话绑定。也就是说,如果客户端会话失效,那么这个节点就会自动被清除掉。注意,这里提到的是会话失效,而非连接断开。另外,在临时节点下面不能创建子节点。
-
顺序节点(SEQUENTIAL) 无序节点
这类节点的基本特性和上面的节点类型是一致的。额外的特性是,在ZK中,每个父节点会为他的第一级子节点维护一份时序,会记录每个子节点创建的先后顺序。基于这个特性,在创建子节点的时候,可以设置这个属性,那么在创建节点过程中,ZK会自动为给定节点名加上一个数字后缀,作为新的节点名。这个数字后缀的范围是整型的最大值。 00010001 00010002 00010003无序节点就是没有顺序
具体组合得到四种:持久有序 持久无序 临时有序 临时无序
2.3.2 入门
2.3.2.1 安装
正常来说应该在linux上面安装,但是现在就在windows安装,学习阶段!
-
官方下载地址:http://mirrors.cnnic.cn/apache/zookeeper/ , 下载后获得,解压即可安装。
-
安装配置: 把conf目录下的zoo_sample.cfg改名成zoo.cfg,这里我是先备份了zoo_sample.cfg再改的名。修改zoo.cfg的值如下:
dataDir=D:/zookeeper-3.4.9/data/data
dataLogDir=D:/zookeeper-3.4.9/data/log
-
启动 :点击bin目录下的zkServer.cmd 这时候出现下面的提示就说明配置成功了。
-
图形界面-ZooViewer:https://blog.csdn.net/u010889616/article/details/80792912
2.3.2.2 代码测试-使用代码创建各种节点
<!-- https://mvnrepository.com/artifact/com.101tec/zkclient -->
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.10</version>
</dependency>
API总结:
-
l new ZkClient(“127.0.0.1:2181”,5000); 创建zookeeper客户端
-
l client.getChildren(“/”):获取子节点 “/”代表根节点
-
l client.createPersistent:创建持久节点
-
l client.createPersistentSequential:创建持久有顺节点,会在path后面增加序号
-
l client.createEphemeral:创建临时节点
-
l client.createEphemeralSequential:创建临时有序节点
-
l client.subscribeChildChanges:订阅子节点的改变
-
l client.subscribeDataChanges:订阅某个节点的数据改变
@Test //持久化节点
public void test1() throws Exception {
//创建客户端
ZkClient client = new ZkClient("127.0.0.1:2181",5000);
//获取根节点
List<String> children = client.getChildren("/");
for (String child : children) {
System.out.println(child); //zookeeper
}
//创建持久节点
client.createPersistent("/zookeeper/createPersistent");
//创建持久顺序节点
String persistentSequential =
client.createPersistentSequential("/zookeeper/createPersistentSequential", "111");
System.out.println("persistentSequential="+persistentSequential);
// /zookeeper/createPersistentSequential0000000003
//创建临时节点
client.createEphemeral("/zookeeper/createEphemeral");
//client.createEphemeral("/zookeeper/createEphemeral"); //重复创建会报错
//创建临时顺序节点
String ephemeralSequential =
client.createEphemeralSequential("/zookeeper/createEphemeralSequential", "111");
System.out.println("ephemeralSequential="+ephemeralSequential);
//关闭
client.close();
}
//测试监听
@Test
public void test3() throws Exception {
//创建客户端
ZkClient client = new ZkClient("127.0.0.1:2181",5000);
if(!client.exists("/yhptest")){
client.createPersistent("/yhptest");
}
//操作节点
client.createPersistentSequential("/yhptest/test","x1");
client.createPersistentSequential("/yhptest/test","x2");
client.createPersistentSequential("/yhptest/test","x3");
client.createPersistentSequential("/yhptest/test","x4");
client.createPersistent("/yhptest/tests","aa");
List<String> children = client.getChildren("/yhptest");
for (String child : children) {
System.out.println(child);
}
//关闭
client.subscribeChildChanges("/yhptest", new IZkChildListener() {
@Override
public void handleChildChange(String s, List<String> list) throws Exception {
System.out.println("子节点改变:"+s);
System.out.println("子节点改变:"+list);
}
});
client.subscribeDataChanges("/yhptest/tests", new IZkDataListener() {
@Override
public void handleDataChange(String s, Object o) throws Exception {
//数据改变
System.out.println("数据改变:"+s);
System.out.println("数据改变:"+o.toString());
}
@Override
public void handleDataDeleted(String s) throws Exception {
System.out.println("数据删除");
}
});
client.delete("/yhptest/tests");
Thread.sleep(2000);
client.close();
}
2.3.3 zk分布式锁-原生
2.3.3.1 非公平锁
根据Zookeeper的临时节点的特性实现分布式锁,先执行的线程在zookeeper创建一个临时节点,代表获取到锁,后执行的线程需要等待,直到临时节点被删除说明锁被释放,第二个线程可以尝试获取锁。
T1 创建临时无序节点goods_2 执行业务逻辑 关闭
T2 创建临时无序节点goods_2 执行业务逻辑 关闭
2.3.3.2 公平锁
public class ZookeeperDistributedLock implements ronghuanetLock {
ZkClient client = new ZkClient("127.0.0.1:2181",
5000);
CountDownLatch cdl = new CountDownLatch(1); //不为零阻塞住,不让他往下走
//父节点路径
String parent = "";
//当前节点路径
String currentPath = "";
//1 goods
// lock_goods_id 父节点(持久节点)
// lock_goods_id_001
// lock_goods_id_002
@Override
public void lock(String resourceName) {
parent = "/"+resourceName;
//判断父节点是否存在,如果不存在要创建一个持久节点
if (!client.exists(parent)){
client.createPersistent(parent,"root");
}
//前面的节点都处理完成,自己变成第一个节点才加锁成功。
if (!tryLock(resourceName)){
lock(resourceName);
}
}
@Override
public void unlock(String resourceName) {
//自己操作完毕,删除自己,让下一个节点执行。
System.out.println(currentPath);
System.out.println(System.currentTimeMillis());
System.out.println(client.delete(currentPath));
client.close();
}
@Override
public boolean tryLock(String resourceName) {
//创建子节点-临时顺序节点
if (StringUtils.isEmpty(currentPath)){
currentPath = client
.createEphemeralSequential(parent + "/test", "test"); //test0001
}
//如果是第一个节点,就获取到锁了。
List<String> children = client.getChildren(parent);
System.out.println(currentPath+"jjj");
for (String child : children) {
System.out.println(child);
}
Collections.sort(children);
///goods_1/test0000000003jjj
//test0000000003
if (currentPath.contains(children.get(0))){
return true;
}else{
//如果不是第一个节点,监听前一个节点,要再这儿等待,知道被触发,再次判断是否是第一个节点进行返回就OK
String str = currentPath.substring(
currentPath.lastIndexOf("/")+1);
System.out.println(str);
int preIndex = children.indexOf(str)-1;
String prePath = parent+"/"+children.get(preIndex);
//监听上一个节点,如果上一个节点被删除,把秒表设置为 0 (cdl.countDown();),那么当前节点取消等待(cdl.await();)重新获取锁
client.subscribeDataChanges(prePath, new IZkDataListener() {
@Override
public void handleDataChange(String dataPath, Object data) throws Exception {
}
@Override
public void handleDataDeleted(String dataPath) throws Exception {
//让他-1 变为零
cdl.countDown();
}
});
//一直等待,直到自己变成第一个节点
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
return false;
}
}
}
2.3.4 zk分布式锁 curator框架实现
- 导入jar
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.1.0</version>
</dependency>
-
配置
@Configuration public class ZkCuratorConfig { //初始化方法start @Bean(initMethod = "start",destroyMethod = "close") //bean声明周期 构造 初始化initMethod 使用 销毁destroyMethod public CuratorFramework curatorFramework(){ //重试策略 RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); //创建客户端 CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); return client; } }
-
代码
@Autowired
private CuratorFramework framework;
@Override
public void updateNum(Map<String,Object> params) {
Long goodsId = (Long) params.get("id");
Integer num = (Integer) params.get("num");
String resourceName = "/goods_"+goodsId;
InterProcessMutex mutex = null;
try{
mutex = new InterProcessMutex(framework, resourceName); //1个信号量
mutex.acquire(3, TimeUnit.SECONDS); //获取一个, 自旋(拿不到一直循环) 适应性自选(拿几秒就算乐 )
System.out.println(Thread.currentThread().getName()+" get lock!");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(System.currentTimeMillis());
//通过商品id获取商品 3
Goods goods = goodsMapper.laodById(goodsId);
System.out.println(goods);
System.out.println(goods.getCount()+":"+num);
if (goods.getCount()>=num){
goodsMapper.updateNum(params);
System.out.println(Thread.currentThread().getName()+"buy "+num+"!");
}
}catch (Exception e){
e.printStackTrace();
}
finally {
if (mutex != null) {
try {
mutex.release();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
基于数据库基本不用,zk或redis要根据项目情况来决定,如果你项目本来就用到zk,就使用zk,否则redis,redis是项目标配 ,并且都是用框架,而不是原生的!